On Writing a watchOS 2 App with Time Travel
Whenever I submit an app to iTunes Connect, be it a bug fix or an update, I get a rush. I submitted my first native Apple Watch app today. I wrote a WatchKit app for iOS last year but this is my first fully native watchOS app.
My app calculates and displays prayer times, which makes perfect sense on a watch. I wanted to use as many features as I could while still making a natural feeling app. I included a complication, which was the culmination of a few weeks of work. In the process, I learned a few things:
- What values are ok to pass as start and end of your timeline?
- What do you need to watch out for when building a time travel timeline?
- What if your complication relies on Core Location?
Performance on Apple Watch requires more effort than iPhone.
For starters, it is perfectly legal and valid to pass [NSDate distantPast]
and [NSDate distantFuture]
to getTimelineStartDateForComplication:withHandler:
and getTimelineEndDateForComplication:withHandler:
.
In fact, if you have a calculated data set, such as mine, it might make sense to. I’ll explain in a minute. First, I want to cover a concept that Eliza Block discussed in her talk from WWDC 2015.
When you’re constructing a timeline of events, your first instinct, like mine was, may be to display your events at the time that they occur. If you do that, you’ll get what looks like an off-by-one error. Events show after they happen. The trick is to show the events immediately after the previous event occurs.
So if you have lunch from 12:00pm to 1:00pm and then a meeting from 4:00pm to 4:30pm, you want to show your meeting at 1:00pm, not at 4:00pm. (This is basically the example Ms. Block gives.)
In my case, I’ve got a list of recurring events that occur every day. I keep them in an array, and then calculate when they occur. Because these events aren’t entered by users, I can calculate as many of them as I want. Passing those [NSDate distant...]
values means I can keep going as long as the watch allows me to.
Another way to do this is to query [CLKComplicationServer sharedInstance]
for it’s earliestTimeTravelDate
and latestTimeTravelDate
. I haven’t seen any difference, as the system clamps the values anyway before asking you to provide a timeline.
As far as properly building a timeline goes, I ran into trouble because I was calculating the times of my events as display data and I also needed to calculate the previous time before it for the proper offset that I discussed before. If I’m at the first item for the day in my list of events, I have to roll over date boundaries which can get messy if done incorrectly. (Think of your date as a cursor which needs to be moved as you calculate the dates.) Eventually, I introduced a cleaner abstraction than the one I was using that encapsulated more information. I wrapped NSDate
and KCZman
(from the KosherCocoa library) inside of a ZMXZmanOccurrence
(a custom class) and we’re good to go.
My data itself relies on the user’s location, because sunrise and sunset are calculated based on the user’s location and the day of the solar year. Fortunately, Core Location is available on watchOS. Unfortunately, calling it too often can kill performance.
Another thing to note is that when the user first starts up your complication, unless they’re in Greenwich, the complication will be showing the wrong time. I haven’t thought of a clever workaround yet, except for maybe pushing data from the phone to the watch.
A final gotcha involves a feature where when the user taps on a prayer time from my list, I show an alert controller with a detailed explanation of how I made the calculation. This is a really cool feature but sometimes showing an alertController causes a spinner to appear over my app. Sometimes it goes away and sometimes the spinner hangs.
I fixed this by cleaning up my viewDidAppear
method in my watch app’s interface controller, but the lesson learned is that the watch is a much tighter environment than the iPhone or iPad, and that it requires even more diligence to keep the runloop lean.
There were two features which I removed because reloading the view seemed so expensive. One was a lighter color theme. I added a second row type in my Storyboard and inverted the colors on it. Adding a menu entry to switch a value in NSUserDefaults is trivial. I also tried animating adding rows to my WKTableView
but hit similar issues. In both cases, the performance was terrible. Spinners and hanging again.
I’m planning to go back to profile in a future sprint, but for now, Ultimate Zmanim 10 is on the way with just one orange-on-dark theme and looks pretty amazing.
This experience was pretty fascinating, frustrating, and at times complicated. But I ended up with a very sharp looking app that will be useful to a specific, if small, demographic.
If you’re developing a watchOS app, I hope this helps you learn a thing or two. Feel free to tweet at me with questions.