The Apple Watch is about to ship to potentially millions of iOS users and we’ve been working really hard to build apps that support it. One of those is The Economist for Apple Watch, which includes a world class audio edition where every article is read by professional voice actors. We wrote extensively about the design of the app here, and why it’s such a great experience for getting your news on Apple Watch. Just as designing an app for a device you’ve never used is a major challenge, so is building an app for a new platform. We wanted to share what it’s really like to build a highly-polished app for smartwatches based on our experience developing The Economist for Apple Watch.
Message Passing on Apple Watch
The first step in building The Economist for Apple Watch was creating MMWormhole. MMWormhole is a message passing library that makes it easy to share information between the WatchKit extension and containing iPhone application to build a rich and interactive Apple Watch app. We knew that our app would need to control audio playback and keep the audio player UI in sync between the two devices. MMWormhole is perfectly suited to this task, which is why we built and open sourced it before a single line of code was written for the watch app. It represents the foundation that our app is built on.
Each screen on the Apple Watch app is driven by content being sent from the iPhone app via MMWormhole. On the player screen we send playback updates with the current article and section, pause states, remaining time, etc. On the tracklist and playlist screens we send the contents of each table as well as the selected index to indicate the currently playing article. Accessing data and passing messages between the phone app and extension is incredibly fast with MMWormhole. We’re even able to drive the remaining time label and progress indicator at pinpoint accuracy with updates that come through the wormhole.
Scheduling UI Updates
It’s very important to structure your watch app’s UI updates to occur after the interface controller’s willActivate method is called. Updating the UI while the interface controller may work, but the results will be inconsistent because this scenario is not officially supported by WatchKit. Some of the issues we’ve noticed are stale data being included in labels and inconsistently populated or blank table cells. Inserting rows in a table while the interface controller is inactive can also result in a crash and is particularly discouraged.
Along similar lines, it’s best to minimize the amount of work you do inside of the willActivate method. The watch shows a loading spinner until the willActivate method, and any UI updates triggered inside of it complete, so making this operation very quick helps your watch app load faster.
Using MMWormhole will actually help with both of these patterns. You can setup your listeners in willActivate, and turn them off in didDeactivate. If your listeners drive your UI updates, you’ve made sure that no updates will happen while the interface controller isn’t active. You’ve also deferred some work until after willActivate completes, because the first listener won’t be fired until the next run loop cycle.
When buttons are tapped on the watch we relay that signal to the phone using the wormhole as well. The responsiveness is excellent. When the pause button is tapped, that action is reflected on the phone within a tenth of a second. It also allows us to keep the phone audio player UI in sync with the watch so that users have a consistent experience. The table views on both the watch and iPhone scroll automatically to the selected article and begin animating our now playing indicator when a new article starts playing. It’s a very integrated and continuous type of experience.
Table Views on Apple Watch
The table views that compose the tracklist and playlist were a particularly difficult development challenge on the watch. Unlike the iPhone, the watch needs to keep all of it’s table cells in memory at a given time, meaning that having large numbers of cells can be a serious performance issue. The severity can depend a lot on the complexity of your table cells and how many groups, labels, and images each cell includes. Dynamic-sized groups and labels have an impact as well.
We made a number of changes to each screen to address this. The full tracklist for an edition might include more than 80 articles across more than a dozen sections. This can take several seconds to fully load, during which time the watch app would be entirely unresponsive. To support that number of cells while still being responsive, we implemented a NSOperationQueue-based system that loads cells in batches, sequentially, until the entire table is loaded. The result is that the first cells populate the table instantly so that users get information quickly, and then subsequent groups of cells are inserted at the bottom of the table every second after that. The benefit is that you can still scroll the table while the rest of the cells are being inserted at the bottom. Inserting a few cells at a time is much faster and less disruptive than trying to load the entire table at once. I expect many developers to adopt this pattern as they fine tune the performance of their watch app.
If you make it down to the bottom of the table before all of the cells are loaded, you see a spinner letting you know that loading is still in progress. Rather than add the spinner as a separate table cell, we simply added an animating image inside of a group below the table. The effect is very similar to the familiar loading indicator below many iPhone table views. When the table finishes loading, we just hide the group with the animating image.
Once you have a lengthy table loaded, the last thing you want to do is re-load it unnecessarily. Every time the selected track changes on the table, we send a message to the interface controller via MMWormhole to tell it which track is now selected. Then we change the now playing indicator for that cell to show it as the selected one and disable the previously selected cell’s indicator. That way, we can avoid reloading the table while still keeping the UI in sync between the watch and phone.
It’s also best to avoid expensive operations like inserting cells into a table while the user isn’t looking at that screen, such as if you switched pages from the tracklist to the playlist. We accomplish this by pausing the operation queue responsible for loading the table when the interface controller deactivates. The same is also true for the player screen. When the player isn’t visible, we pause the MMWormhole listeners responsible for updating the remaining time and progress indicators so that the watch extension isn’t doing any more work than absolutely necessary. This helps keep the UI responsive and performant.
Image Caching and Performance
Located in the force touch menu on each screen is the ability to change the selected edition. This presents a modal interface controller with a table that includes a row for each edition with available audio content. In addition to the edition title and date, we are also showing the cover image for each edition.
The watch includes an image cache for each app that we can use to store the covers once the edition is downloaded on the phone. There are two key tips to make working with this image cache fast and efficient. It is best to resize the image programmatically on the phone to the exact size needed for the watch. Even if you have a fairly small 200×200 thumbnail available, you need to shrink it down to the image view size (40×40 for example) before caching it on the watch. Caching and loading images is also a great place to move some work off of the main queue. You should move calls to addCachedImage onto a background queue. That makes it faster to populate the table and show the basic UI without waiting for all the images to load. After the call to addCachedImage, dispatch back to the main queue and call setImageNamed to populate the image view. That will be a very fast operation because the image will already have been sent asynchronously to the watch. While this is all happening, we still show a spinner in place of the image view to let the user know that activity is still in progress.
Speaking of images, one of the best ways to optimize both the performance of your app as well as the storage footprint on the watch is to use the watch app’s asset catalog. The asset catalog gets copied to the watch when the app is installed, meaning that none of it’s images need to be transferred to the watch when you ask to display them. To make sure you aren’t accidentally transferring images to the watch programatically, be sure to use the setImageNamed method instead of the setImage method. Setting the image name tells the watch to look in it’s image catalog first, or for a cached image second to fulfill that request.
Apple also added support for different sized images for each of the two sizes of Apple Watch. If you use the asset catalog to specify an image for each size of device then you will avoid having duplicate images being stored on the watch because only the 42mm image will be copied to the 42mm watch, and vice-versa. This is a great way to respect the limited space customers will have on their watch and make sure your app will install as quickly as possible.
The best advice I have for building a great Apple Watch app is to start now. WatchKit has plenty of limitations, but you can still build an amazing app with it. Don’t be afraid to think outside of the box and try new things. And remember your basic troubleshooting and performance tips. When you notice something having trouble, put in log statements and start disabling features until you isolate the problem. In the end you’ll learn a lot about developing for the watch and create something your users will love.