Categories

Since both the new app extensions of iOS 8 and Swift are both fairly new, I created a sample app that demonstrates how a simple Today extension can be made for iOS 8 in Swift. This widget will show the latest blog posts of the Xebia Blog within the today view of the notification center.

The source code of the app is available on GitHub. The app is not available on the App Store because it’s an example app, though it might be quite useful for anyone following this Blog.

New Xcode project

Even though an extension for iOS 8 is a separate binary than an app, it’s not possible to create an extension without an app. That makes it unfortunately impossible to create stand alone widgets, which this sample would be since it’s only purpose is to show the latest posts in the Today view. So we create a new project in Xcode and implement a very simple view. The only thing the app will do for now is tell the user to swipe down from the top of the screen to add the widget.

Time to add our extension target. From the File menu we choose New > Target… and from the Application Extension choose Today Extension.

We’ll name our target XebiaBlogRSSWidget and of course use Swift as Language.

The created target will have the following files:

TodayViewController.swift

MainInterface.storyboard

Info.plist

Since we’ll be using a storyboard approach, we’re fine with this setup. If however we wanted to create the view of the widget programatically we would delete the storyboard and replace the NSExtensionMainStoryboard key in the Info.plist with NSExtensionPrincipalClass and TodayViewController as it’s value. Since (at the time of this writing) Xcode cannot find Swift classes as extension principal classes, we also would have to add the following line to our TodayViewController:

[objc]
@objc (TodayViewController)
[/objc]

Update: Make sure to set the “Embedded Content Contains Swift Code” build setting of the main app target to YES. Otherwise your widget written in Swift will crash.

Add dependencies with cocoapods

The widget will get the latest blog posts from the RSS feed of the blog: https://xebia.com/blog/feed/. That means we need something that can read and parse this feed. A search on RSS at Cocoapods gives us the BlockRSSParser as first result. Seems to do exactly what we want, so we don’t need to look any further and create our Podfile with the following contents:

It’s important to only add the dependency to the XebiaBlogRSSWidget target since Xcode will build two binaries, one for the app itself and a separate one for the widget. If we would add the dependency to all targets it would be included in both binaries and thus increasing the total download size for our app. Always only add the necessary dependencies to both your app target and widget target(s).

Note: Cocoapods or Xcode might give you problems when you have a target without any Pod dependencies. In that case you may add a dependency to your main target and run pod install, after which you might be able to delete it again.

The BlockRSSParser is written in objective-c, which means we need to add an objective-c bridging header in order to use it from Swift. We add the file XebiaBlogRSSWidget-Bridging-Header.h to our target and add the import.

[objc]
#import "RSSParser.h"
[/objc]

We also have to tell the Swift compiler about it in our build settings:

Load RSS feed

Finally time to do some coding. The generated TodayViewController has a function called widgetPerformUpdateWithCompletionHandler. This function gets called every once in awhile to ask for new data. It also gets called right after viewDidLoad when the widget is displayed. The function has a completion handler as parameter, which we need to call when we’re done loading data. A completion handler is used instead of a return function so we can load our feed asynchronously.

We assign the result to a new optional variable of type RSSItem array:
[objc]
var items : [RSSItem]?
[/objc]

The completion handler gets called with either NCUpdateResult.NewData if the call was successful or NCUpdateResult.Failed when the call failed. A third option is NCUpdateResult.NoData which is used to indicate that there is no new data. We’ll get to that later in this post when we cache our data.

Show items in a table view

Now that we have fetched our items from the RSS feed, we can display them in a table view. We replace our normal View Controller with a Table View Controller in our Storyboard and change the superclass of TodayViewController and add three labels to the prototype cell. No different than in iOS 7 so I won’t go into too much detail here (the complete project is on GitHub).

Since items is an optional, we use Optional Binding to check that it’s not nil and then assign it to a temporary non optional variable: let items. It’s fine to give the temporary variable the same name as the class variable.

In our storyboard we’ve set the type of the prototype cell to our custom class RSSItemTableViewCell and used RSSItem as identifier so here we can dequeue a cell as a RSSItemTableViewCell without being afraid it would be nil. We then use Optional Binding to get the item at our row index. We could also use forced unwrapping since we know for sure that items is not nil here:
[objc]
let item = items![indexPath.row]
[/objc]

But the optional binding makes our code saver and prevents any future crash in case our code would change.

Since the widgets all have a fixed width, we can simply specify 0 for the width. The height is calculated by multiplying the number of rows with the height of the rows. Since this will set the preferred height greater than the maximum allowed height of a Today widget it will automatically shrink. We also add the sectionFooterHeight to our calculation, which is 0 for now but we’ll add a footer later on.

When the preferred content size of a widget changes it will animate the resizing of the widget. To have the table view nicely animating along this transition, we add the following function to our class which gets called automatically:

Here we simply set the size of the table view to the size of the widget, which is the first parameter.

Of course we still need to call our update method as well as reloadData on our tableView. So we add these two calls to our success closure when we load the items from the feed

[objc]
success: { feedItems in
self.items = feedItems as? [RSSItem]

self.tableView.reloadData()
self.updatePreferredContentSize()

completionHandler(.NewData)
},
[/objc]

Let’s run our widget:

It works, but we can make it look better. Table views by default have a white background color and black text color and that’s no different within a Today widget. We’d like to match the style with the standard iOS Today widget so we give the table view a clear background and make the text of the labels white. Unfortunately that does make our labels practically invisible since the storyboard editor in Xcode will still show a white background for views that have a clear background color.

If we run again, we get a much better looking result:

Open post in Safari

To open a Blog post in Safari when tapping on an item we need to implement the tableView:didSelectRowAtIndexPath: function. In a normal app we would then use the openURL: method of UIApplication. But that’s not available within a Today extension. Instead we need to use the openURL:completionHandler: method of NSExtensionContext. We can retrieve this context through the extensionContext property of our View Controller.

More and Less buttons

Right now our widget takes up a bit too much space within the notification center. So let’s change this by showing only 3 items by default and 6 items maximum. Toggling between the default and expanded state can be done with a button that we’ll add to the footer of the table view. When the user closes and opens the notification center, we want to show it in the same state as it was before so we need to remember the expand state. We can use the NSUserDefaults for this. Using a computed property to read and write from the user defaults is a nice way to write this:

This allows us to use it just like any other property without noticing it gets stored in the user defaults. We’ll also add variables for our button and number of default and maximum rows:

[objc]
let expandButton = UIButton()

let defaultNumRows = 3
let maxNumberOfRows = 6
[/objc]

Based on the current value of the expanded property we’ll determine the number of rows that our table view should have. Of course it should never display more than the actual items we have so we also take that into account and change our function into the following:
[objc]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let items = items {
return min(items.count, expanded ? maxNumberOfRows : defaultNumRows)
}
return 0
}
[/objc]

Caching

At this moment, each time we open the widget, we first get an empty list and then once the feed is loaded, the items are displayed. To improve this, we can cache the retrieved items and display those once the widget is opened before we load the items from the feed. The TMCache library makes this possible with little effort. We can add it to our Pods file and bridging header the same way we did for the BlockRSSParser library.

Since the RSSItem class of the BlockRSSParser library conforms to the NSCoding protocol, we can use them directly with TMCache. When we retrieve the items from the cache for the first time, we’ll get nil since the cache is empty. Therefore cachedItems needs to be an optional as well as the downcast and therefore we need to use the as? operator.

We can now update the cache once the items are loaded simply by assigning a value to the property. So in our success closure we add the following:
[objc]
self.cachedItems = self.items
[/objc]

And then to load the cached items, we add two more lines to the end of viewDidLoad:
[objc]
items = cachedItems
updatePreferredContentSize()
[/objc]

And we’re done. Now each time the widget is opened it will first display the cached items.

There is one last thing we can do to improve our widget. As mentioned earlier, the completionHandler of widgetPerformUpdateWithCompletionHandler can also be called with NCUpdateResult.NoData. Now that we have the items that we loaded previously we can compare newly loaded items with the old and use NoData in case they haven’t changed. Here is our final implementation of the success closure:

And since it’s Swift, we can simply use the != operator to see if the arrays have unequal content.

Source code on GitHub

As mentioned in the beginning of this post, the source code of the project is available on GitHub with some minor changes that are not essential to this blog post. Of course pull requests are always welcome. Also let me know in the comments below if you’d wish to see this widget released on the App Store.

Hey so I am doing the “Add Dependencies With Cocoapods” and I want to import the cocoapods into both my original project and my extension. However, I am getting “duplicate symbol” for every symbol from my pods… how would I avoid this, any ideas? Thanks.

I tried adding this kind of widget to my blog app. I followed all of the steps, and the widget adds, but never shows as a table view, just a row in the notification center. I even went as far as to copy all the files from the github file into my own, and it still doesn’t work. Am i Missing something?

For people experiencing similar problems as Tyler:
The widget was crashing silently. This happened because the main target was running. When running the Widget target, Xcode will crash in the debugger.
The crash itself was caused because the “Embedded Content Contains Swift Code” build setting was set to NO (default) and had to be YES.

Sure, you could skip the caching entirely but then nothing is displayed when the widget is opened. You could also persist to disk yourself or use another caching mechanism. As long as it’s still available after the widget stops running.