Right from the start
With Apple Watch shipping this week, we wanted to share some important lessons we have learned so far about building Apple Watch apps. One of our clients had already shipped Android Wear functionality in their flagship app, so when Apple Watch was announced, they had somewhat of a head start thinking through the aspects of their app that might translate to wearables. That said, we1 took the opportunity to rethink with our client what made sense on Apple Watch, taking cues from what Apple has shown thus far. We wanted both the aesthetics and the functionality to feel appropriate for the platform.
All new form factors introduce new challenges, and I’m going to focus on one particular challenge that took us a good bit of time to get right: performance. We started working on this wearable project as soon as Apple released the Xcode beta that included WatchKit last fall, and it’s a good thing we did, because over the past few months we’ve gone through multiple rewrites to get the code architecture and performance where we wanted it to be.
“Third party apps on the Apple Watch. They suck. They’re really, really slow.”John Gruber
Nobody rides for free
One of the primary reasons to obsess over performance in your Apple Watch app is this: if you think people have a short attention span using their iPhone, wait until you see how little time they’ll spend looking at their new Apple Watch. This is partly by Apple’s own design, as the watch goes to sleep within about 15 seconds without any interactions.
Many of the early Apple Watch reviews (The Verge, Reuters, CNET, Daring Fireball) state how most third-party apps feel slow, which did not surprise us at all, as we’d absolutely gone down the wrong path several times and ended up with poor performance on our early versions. However, after each iteration, we would try again with a different approach until we finally produced a version of the app that felt good to use. After all of that effort, we can confidently say it’s possible to achieve reasonable performance from a third-party app, but here is the key: you must be willing to take a different approach than you would in an iOS app.
First, it is important to highlight one of the key constructs of third-party Apple Watch apps: they do not run natively on the watch, but rather run on the phone and send the data and screens to the watch via standard bluetooth. Apple provides several tools to help mitigate this, including both image caching and local storyboards on the watch itself, but even with those tools, third party apps start behind the 8 ball on performance before you even start doing any real work.
Even the simplest of screens with a few labels and a few buttons will take half a second or more to initially send to the Watch. Add in lists of things, such as a simple 10 to 15 row table, and it gets even slower, perhaps a second or more. If it happens to be a paged app that has 3 to 5 controllers, it’ll slow down even further. And if you attempt to have more complex layouts due to design requirements? You get the idea…before you know it, what seems like a simple app with a handful of screens can be littered with little second-or-more delays as each screen is sent to the watch the first time. Apple provides plenty of valuable tips at developer.apple.com/watchkit/tips/, and we highly recommend reading and understanding them thoroughly. If you follow these tips, you will still have some small delays rendering screens, but it will become much more tolerable.
The next and more complicated issue was adding dynamic data and images. Apple recommends that WatchKit extensions offload the bulk of the work (like networking operations) to your iPhone app, and they provide a nice api for doing so –
openParentApplication:reply:. So our initial workflow was as simple as this: user launches our watch app, which talks via bluetooth to our WatchKit extension, which then asks for data from our iPhone app, which wasn’t running yet in the background on the phone;
openParentApplication:reply: is called, which launches the app, which then takes the watch’s request and calls an API on our server, which processes the request and hands a response back to the app, which processes the response and hands the data to the extension, which then renders the table cells and sends them back to the watch! OK, perhaps that wasn’t simple, but it worked…but how did it perform?
Apple was kind enough to set up labs so that registered developers could test their apps on a real Apple Watch, and we scheduled a visit immediately. Our excitement was quickly tempered as we found ourselves staring at the watch waaaaaaay too long before seeing pixels. As our Design Director, Rusty Mitchell, noted in a previous post on the psychology of waiting, even short wait times can have a significant impact in the number of users willing to stick with your app or service.
We quickly learned that even delays that feel tolerable on the iPhone feel painfully slow on the Apple Watch. The above approach just wasn’t gonna work for our app. It’s easy to point the finger at Apple and blame WatchKit for being slow, but at the end of the day, users simply aren’t going to use apps that are slow, regardless of who is to blame. Disappointed, we returned home determined to do better, both on UI and performance.
After scratching our heads a bit, we had a minor epiphany: we had to stop viewing the watch app as a smaller version of our iPhone app, and instead simply consider it as another view of the iPhone app’s data. Taking the same traditional iPhone app approach of requesting data as the user interacts with the app just simply wasn’t going to work. We had to move to a model where our iPhone app would write out watch data and images proactively (both during usage and with background tasks) so that when the user opens the app on their watch, the watch app simply has to render data it has already received, with no server round trips required. For many apps, this approach likely will take lots effort to do properly, and likely will require a rethinking of what data is retrieved and when, but it had a dramatic impact on the usability of our app.
So how did we do it? Our app’s data layer is fairly large, and to bring it over to our WatchKit extension would make it tough to keep our WatchKit code clean and lightweight. Plus, our watch app only shows a very small subset of our data, both in quantity of data items and in the amount of info about each item. WatchKit provides several options for sharing data between your iOS app and your WatchKit extension. Due to the amount of data and images, we chose to use “shared containers,” which is essentially a directory that both your processes can write to and read from.
Following the KISS principle, we decided to simply use basic lightweight JSON files for data and jpg encoded NSData files for images and save them both to the shared container. Then in the WatchKit Extension, we have a very simple model class that consumes the JSON and binds it to our watch screens. The crucial part of the model class is that it contains only about 10-15% of the fields that might be on the full blown persistence models on the app side. Each model in the app that wants to exist on the watch implements a conversion method that will create the thin JSON version of itself for the watch.
We then added triggers in our iOS app that populate watch data anytime it’s needed, be it new data from server, updates from user, etc. Any event or action that affects what might be displayed on the watch triggers the new objects and images to be written to the shared container. We also implemented background tasks that do the same thing so that the data stays as fresh as possible, even if the user isn’t regularly running the app. Because the lightweight data and images are already saved to the shared container, the WatchKit extension simply has to render the screens when the user launches the app, without the need for any additional server trips or image downloading.
She’s like the wind
Once we implemented the above approach, our app instantly became fun to use and no longer felt dramatically slower than the built in apps on the Watch. With a few more trips to the Apple labs, we confirmed that our new approach works well, and we are very confident in our first app release.
While this exact implementation might not work (or be relevant) to your app, I suspect with enough effort and crafty thinking you can figure out ways to at least work towards this approach. The key is to explore creative ways to have your watch app and iPhone app work together rather than viewing the Watch app as a miniature version of your iPhone app.