One of the exciting new things announced at the WWDC keynote this month was App Extensions. This really opens up inter-app communication as well as letting developers do interesting things with the notification center and sharing.
I’m convinced that pretty much every news organisation is going have a widget included in their app to show the latest couple of stories. To that end, I’m going show here how I added a Today widget to a simplified version of the Broadsheet app.
All the code talked about here is available on GitHub under the MIT license.
This all works in the simulator but has not been tested on a device yetKindly tested by Liam Dunne and it works (phew!)
- Some of the cell layouts are a bit off but are good enough for informational purposes
- This is still all in Obj-C rather than being all cool and done in Swift
Our starting point is a simple app that grabs the JSON feed of the latest 10 posts Broadsheet.ie and displays them in a UITableView. Tapping on a cell brings you to a screen that displays the content of the post in a UITextView (this will sometimes be mangled due to the content).
To this app we’re going to add a Today widget that displays the latest two post and brings you back to the app when a cell is tapped.
The base app is in the (imaginatively called) ‘baseapp‘ branch.
Creating The Framework
First, we’re going to create a framework to hold the common files that will be used by both the app and the widget. We need to add a framework target to the project by doing the following:
- Select the project
- Editor->Add Target
- Select ‘Framework & Library’
- Select ‘Cocoa Touch Framework’ and hit ‘Next’
- Give it a name (ours is called CBPKit) and hit ‘Finish’
The framework should be automatically included in your app target (check under ‘Link Binary With Libraries’ in ‘Build Phases’).
Move any files you want your framework to provide from your app to the framework (note that this doesn’t move the files on disk). In this case it is everything but the view controllers.
Select the headers and in the ‘Utilities’ pane select your framework and set the option on the right to ‘project’.
Select the implementation files and again in the ‘Utilities’ pane select your framework and unselect your project.
For displaying the images in the cells, I’m using SDWebImage. I could have just add the relevant SDWebImage files to the framework I just created, but this seems wrong to me as:
- You may include another framework in the future that includes and exposes SDWebImage and then you’d have a clash
- You release your framework and someone can’t use it because your included version of SDWebImage clashes with the version they want to use
- SDWebImage may release its own framework in the future
Instead, I created a separate framework (with the exact same steps as above) called SDWebImageKit.
As I may want to use this framework in another project, I set the headers to ‘Public’ in the ‘Utilities’ pane and added each of the headers to the framework header file. This means that I can import all the headers at once using:
When you run the app now, it should look no different to before despite the internal changes.
If you switch to the ‘framework‘ branch, you’ll see this built on top of the previous branch.
Like the framework, we first add a Extension target to the project.
- Select the project
- Editor->Add Target
- Select ‘Application Extension’
- Select ‘Today Extension’ and hit ‘Next’
- Put in a name (ours is called CBPTodayExtensionExample) and hit ‘Next’
- Go into Build Phases and include both the frameworks from above in the ‘Link Binary With Libraries’
If you run the project now in the simulator and pull down the notifications drawer, you should see ‘1 New Widget Available’ at the very bottom. Tapping on ‘Edit’ will bring up all the available widgets and the new widget should be at the bottom with a green plus beside it. Tap the plus and it should be displayed with any other active widgets when you hit ‘Done’.
‘Hello world’ is a nice greeting but we want to replace this with a table containing 2 tappable cells containing the latest two posts from Broadsheet.
I don’t use storyboards, so I removed the default one added by the template. In info.plist I removed the key NSExtensionMainStoryboard and replaced it with NSExtensionPrincipalClass and put ‘TodayViewController’ as the value.
The cell I use in the app is a bit big for displaying in the notification center, so I added a set of new constraints to display a more compact cell. This means I can reuse the same data source and cell from the main app in my Today extension with just some minor modifications.
In the TodayViewController, there are two places that need to load data from the network – when the widget is created and when widgetPerformUpdateWithCompletionHandler is called. For the former, I load posts in viewDidLoad, so that they should be ready by the time the widget displays. When iOS thinks the widget will be displayed to the user after it has been first displayed, widgetPerformUpdateWithCompletionHandler is called giving the widget a chance to update the posts displayed.
In this example, both cells are always reloaded even if they already contain the posts returned. It probably shouldn’t do this and instead only replace the updated cells, but that’s a bit of extra polish I’ll leave for when building the full widget.
To get the app to open when a cell is tapped, a custom URL scheme is registered in the base app and this can be called using the UIViewController’s extensionContext to open the URL.
If you switch to the ‘todayextension‘ branch, you’ll see this built on top of the framework branch.
To keep everything tidy (and to satisfy my developer OCD), I used Synx to synchronise the files and folders to match those used in the project.
That’s all that’s needed to get a Today widget up and running. I’m delighted it’s so straight forward and I’m looking forward to seeing what people do with them.
There’s an issue with the debugger in Xcode attaching to the extension process properly. You can manually attach it by selecting ‘Debug’->’Attach to Process’ and finding the extension process under ‘System’. If your extension crashes on startup you won’t be able to this.
If you want to see logging from before you reattached the debugger, you can view it in the system log. You can open it from ‘Debug’->’Open System Log…’. If you filter it by your extension’s bundle id you’ll get the relevant entries.
Update 18/6/2014: In the release notes for Xcode 6 Beta 2 there’s a list of issues around the debugger and extensions. One pertinent to this app is that you can’t debug a today widget in the simulator but you can on device.
Update 7/7/2014: Xcode 6 Beta 3 came out today and fixes some of the debugging issues.