Building Watch Apps
I’ve been tinkering with WatchKit in my spare time and thought I’d share a bit of what I’ve learned. Of course, all of my tinkering was done in the simulator as I’ve not actually played with an Watch.
Watch Apps Have Up To Three Components
Watch apps contain an application, an optional glance, and optional custom notifications.
Glances are sort of like Today widgets on iOS. The idea is to show the most important bit of information your app provides. For example, CNN might display breaking news. The Watch may create your glance controller some time before the user sees it so it’s important to make sure the information displayed is up to date in willActivate(). Glances can be enabled or disabled by the user, don’t support scrolling, and allow for limited interactivity. Tapping a glance opens your Watch application.
Notifications are baked in with a default look but you can choose to tailor them to your app.
Two Parts In A Whole
WatchKit apps are made of two parts: an application and an extension. I better understood how to go about building Watch apps when I realized that there are two main threads. It’s obvious in retrospect, but there’s a main thread in the Watch application and a main thread in the Watch extension. If you block the main thread in the Watch extension, the user can still navigate the app interface on the Watch but UI changes will not appear. This happens because changes to interface objects are coalesced and final states are sent to the Watch app at the end of the run loop cycle. You prevent that from happening if you block the main thread. Because of this, it’s incredibly important to keep the main thread in the extension unblocked.
The application doesn’t contain any code. Instead, it contains a mandatory storyboard that defines your interface layout (including glance and notification layouts) as well as whatever assets you want to display on the Watch. You can include sequences of images if you would like to add animations. Apps are installed via the “Apple Watch” companion app included with iOS 8.2 and newer. The Watch uses some standard runner to start your application and the Watch extension on your phone when a user starts taps your icon on the Watch’s home screen. Touch events are forwarded to the extension.
The Watch extension contains your code and runs on your phone. Apple cleverly handles communication between the application and extension via Bluetooth and/or WiFi. You do not need to explicitly call APIs to send or receive data from the Watch. This makes writing apps much simpler than it might otherwise be but it’s important to keep in mind what’s actually happening and to minimize UI updates to keep the interface responsive. Sending large images, for example, can make your app appear slow.
The Watch extension is short lived. When a user closes your app on the Watch, the extension is stopped. Because of this, one should take care when loading and sharing data in the Watch extension.
Loading and Sharing Data
Since Watch extensions are short lived, it’s a good idea to load data using the parent application rather than making network requests directly from the Watch extension. You can do this with a handy class function on WKInterfaceController called openParentApplication(_:reply:). By way of IPC, this method will open your application in the background, if needed, and pass along the userInfo you supply. The parent application’s UIApplicationDelegate instance will then receive a call to application(_:handleWatchKitExtensionRequest:reply:) and can load data then send it to the Watch extension by calling the reply() block. Don’t forget to start a Background Task.
It might also be worth periodically caching data in a shared directory using the parent application, depending on your needs. This is useful in cases where you would like to show data to the user as quickly as possible and simultaneously fetch fresh data.
For sharing small amounts of data, NSUserDefaults may be sufficient. @drewconner first suggested this to me and I later found that Apple recommended the same. Otherwise, you can write files, use Core Data, etc. Look up App Groups and Keychain sharing if you’re interested in sharing persisted data between your parent application and Watch extension.
Interface and Extension
The interface is defined in a Watch app’s storyboard. Other than table rows, layouts are predefined.
WKInterfaceTable can define more than one type of row. Rows can be created dynamically and updated. Unlike UITableView, where rows are created on demand and reused, rows for WKInterfaceTable are created prior to being displayed. Because of this, Apple recommends using twenty rows or less to avoid rendering delays.
Layouts must be defined ahead of time in the storyboard. You can’t create a button at runtime and add it to your layout, for example. You can, however, create a table row containing a button at runtime if you’ve defined that row in the storyboard at build time.
You can hide and show UI elements at runtime. You should use this functionality sparingly since all elements, including hidden elements, must be created before your interface is displayed.
You cannot query the current state of a UI component as no getters are exposed on WKInterfaceObject. Credit to @javi for pointing this out.
I wondered why this might be. A naive getter implementation might need to communicate with the Watch app to query the state of a control. That would have a major costs: if it were asynchronous then it would be difficult to use and if not then it would block the main thread in the extension. A more interesting approach might be to mirror the UI state in the extension and expose that while keeping it in sync with what’s displayed on the Watch. That’s tricky but technically possible to implement (something akin to what React JS or React Native do). I’ve not found this limitation to be much of an issue in practice.
The layout model is refreshingly simple. It’s similar to Swing’s BoxLayout or Android’s LinearLayout. Containers are called groups and can be nested. Groups can lay out children either horizontally or vertically. Buttons can be groups.
The following two enhancements would make more interesting layouts possible: First, replace group insets with margins and allow negative values. This would make overlapping layouts possible. Second, add the ability to disable content clipping on groups.
Images and Caching
Images not included in the application bundle must be sent to the watch wirelessly. This is not necessarily fast. Resize large images and compress them using UIImagePNGRepresentation() or UIImageJPEGRepresentation() prior to use. Kudos to my friend Tom Tuling for suggesting this. Conveniently, WKInterfaceImage supports setting image data directly.
WKInterfaceDevice exposes a few simple APIs for caching up to 20MB of frequently used images on the Watch. Unlike NSCache, you are entirely responsible for eviction. Attempting to add an image when the cache is full will fail. The only metadata kept by the cache are the key and image size. You’ll need to keep additional metadata to implement all but the most basic eviction policy. For example, you could map key to insertion date if you wanted to implement LRU. You don’t need to cache bundled images.
Handoff lets you expose data about your app’s current state via an API on WKInterfaceController (updateUserActivity(_:userInfo:webpageURL:)). Your parent app can use this information to show some particular thing such as an email or tweet that is currently displayed on the Watch.
I suggest taking a look at Apple’s WatchKit development tips. Thanks to @schukin for the link.
Learning WatchKit was fun, in part, because the APIs are simple and limited. That might seem counter-intuitive but creating modern iOS apps is quite complicated. There’s a lot to consider in the UI layer, let alone networking, concurrency, persistence, notifications, permissions, entitlements, background behavior, and more. I suppose Watch apps add to that complexity but I don’t mind. I also enjoyed learning WatchKit because it gave me an opportunity to learn Swift. That’s a story for another post.