At Contentful we’re suckers for craft beers. We’re always looking for new bars where they’re serving some beautiful hops around Berlin. We also pride ourselves with making it easy to manage content on any device, so when Apple recently released the first beta development kit for their upcoming watch called WatchKit, we had no doubts on what to build.
Brew is an example app that is using the Contentful Content Delivery API and shows nearby bars serving craft beers. Not only for Berlin, but also in NYC and SF (which many of us frequent).
The app shows you a list of bars based on your location and allows you to drill-down for more information, like what types of beer they serve, opening hours, etc., as well as images and a map with the bars location.
Implementation
To work with Watchkit, you will need the Xcode 6.2 beta.
The apps currently possible to make consist of an iOS extension, which contains the actual logic, as well as a small binary that runs on the watch and displays a Storyboard and contain static images. WatchKit basically abstracts a Bluetooth connection to the watch which fills and updates the fixed user interface on the watch. This is also why its classes have only setters and no getters. Animations are entirely based on image sequences and there is no custom drawing.
In addition to that, there is also the possibility to create actionable notifications and glances for your watch application, with fully native apps coming "later in 2015" according to Apple.
Using the Contentful SDK
The SDK needs to be linked to the WatchKit extension and can then be used just as in a normal iOS app.
If you use CocoaPods, just define it as a dependency for the correct target:
1 2 3 4 5 | target 'WatchKitExample WatchKit Extension' do pod 'ContentfulDeliveryAPI' end |
If you are using Swift, you will need to create a bridging header:
1 | #import <ContentfulDeliveryAPI/ContentfulDeliveryAPI.h> |
and set that as "Objective-C Bridging Header" for the given target in your build settings.
WKInterfaceController
Similar to a view controller, a WKInterfaceController
is responsible for updating the UI and managing segues between interface controllers. Passing data is done by the various contextForSegue...
methods, the returned context value will be passed to the next controller via awakeWithContext
and is an optional AnyObject
. There are also two lifecycle methods willActivate
and didDeactivate
which are called once an interface controller is being displayed or respectively hidden.
Speaking of the Interface Builder side of things, if one controller should display more content than what fits on a single screen, just add more elements and vertical scrolling will be enabled automatically.
WKInterfaceGroup
The static layout is built using nested groups, which can either layout their contents horizontally or vertically. By arbitrary nesting, slightly more complex layouts can be achieved. It can contain an optional background image or an animation.
WKInterfaceTable
If you want to present a list of arbitrary length to the user, a table is your familiar friend. It is also the only truly dynamic element in WatchKit, even allowing you to insert or remove rows after the fact.
The following code sample shows how a table is filled inside our app, with dynamic data coming from Contentful:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | client = CDAClient(spaceKey: "exembnlnz9oo", accessToken: "2ec43b32ffdda511b09abfd6a5b8ff65125cd19a4f6377d6a1e9540d34120052") client.fetchEntriesMatching(["content_type": "6LmYY0rGhOaUyweiwSm4m", "order": "-sys.createdAt", "fields.visible": true], success: { (response, array) -> Void in self.newsItems = array.items as [CDAEntry] self.newsTable.setNumberOfRows(countElements(self.newsItems), withRowType: "NewsTableRowController") for (index, entry) in enumerate(self.newsItems) { let row = self.newsTable.rowControllerAtIndex(index) as NewsTableRowController row.interfaceLabel.setText((entry.fields as NSDictionary)["nameOfBar"] as? String) } }) { (reponse, error) -> Void in NSLog("@Error: %@", error) } |
Essentially, we can set the number of desired rows and then update the UI for corresponding row controllers. Contrary to what the naming would make you believe, those are custom NSObject
subclasses which act as a simple container for the interface elements of a single row.
WKInterfaceImage
This element can display a single image or an image animation. Be aware that the setImageNamed:
method allows you to utilize a cache on the watch, even if used with images which were not part of the initial bundle that was deployed.
For our example, we wanted to integrate the standard WatchKit activity indicator while images are loaded from the server, without having to manually recreate it.
Looking a bit into the new private frameworks added in Xcode 6.2, PepperUICore appears to be the UI framework for the watch, and in there we can find activity-.png* images, which simply contain all animation states horizontally spread in one image. Unfortunately, there is no public API for turning those into an animated UIImage
, so this little piece of code will cut it into individual images and then create an animated image from the array:
1 2 3 4 5 6 7 8 9 | let sheet = UIImage(named: "activity-medium") var images = [UIImage]() for (var currentX = 0; currentX < Int(sheet!.size.width); currentX += 30) { let splitRef = CGImageCreateWithImageInRect(sheet?.CGImage, CGRect(x: CGFloat(currentX), y: 0.0, width: 30.0, height: sheet!.size.height * 2)) images.append(UIImage(CGImage: splitRef)!) } let image = UIImage.animatedImageWithImages(images, duration: 0.1) |
WKInterfaceMap
As navigation will supposedly be a big part of the watches user experience, displaying map tiles is also part of the API. The view will be entirely non-interactive, though, sending the user to the built-in application upon tapping it. In addition to adding annotations for the points of interest, the area of the map needs to be specified using setMapRect
and setCoordinateRegion
will set the center point and zoom.
1 2 3 4 5 6 7 | let coordinateSpan = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005) let location = self.context.CLLocationCoordinate2DFromFieldWithIdentifier("location") mapView.addAnnotation(location, withPinColor: .Red) mapView.setVisibleMapRect(MKMapRect(origin: MKMapPointForCoordinate(location), size: MKMapSize(width: 0.5, height: 0.5))) mapView.setRegion(MKCoordinateRegion(center: location, span: coordinateSpan)) |
It is important to call setRegion
after setVisibleMapRect
, because otherwise the result will be unexpected.
Communication between app and extension
As the communication between phone and watch is done in an extension, by default there will be no way to talk to the main application on the phone. For our example app, we already required that, though, because the extension cannot access the user’s location.
Thankfully, Apple added App Groups to iOS 8 and with them the ability to share NSUserDefaults
between multiple processes in that same group. App Groups can be configured in the “Capabilities” section of the build target where Xcode will do most of the work for you, including generating an entitlements file.
Now we just need to store the user’s location when the main app is opened:
1 2 3 4 | var location = newLocation!.coordinate let userDefaults = NSUserDefaults(suiteName: "group.com.contentful.WatchKitExample") userDefaults!.setValue(NSData(bytes: &location, length: sizeof(CLLocationCoordinate2D)), forKey:"currentLocation") userDefaults!.synchronize() |
and we can extract that information when the watch app gets activated:
1 2 3 4 5 6 7 8 | override func willActivate() { let userDefaults = NSUserDefaults(suiteName: "group.com.contentful.WatchKitExample") userDefaults!.synchronize() let locationData = userDefaults!.dataForKey("currentLocation") var location = CLLocationCoordinate2D(latitude: 0, longitude: 0) locationData!.getBytes(&location, length: sizeof(CLLocationCoordinate2D)) } |
Other ways of communication are shared files or the Darwin notification center API. Both methods are wrapped nicely in the MMWormhole library which you might want to check out if you need to share more than one piece of data.
With this overview of WatchKit you are well-equipped to add a watch extension to your existing Contentful-powered applications and be ready when the ᴡᴀᴛᴄʜ ships early next year. You can also checkout the example code for the app on GitHub.