In an effort to reduce loading times for 3rd party watch apps, for a page-based interface, Apple has changed the calling pattern for willActivate() and didDeactivate(). In essence, WatchOS will more aggressively call willActivate() on your WKInterfaceController even if it is not visible to cache the view so it can be displayed immediately when you swipe over to it.

While this is effective in reducing loading spinners that users will see, it can cause bugs based on some common assumptions that developers might make.

Let's first take a moment to understand deeply the change in the calling pattern.

In WatchOS 1.0, Apple would call willActivate() only just before the page is visible, and didActivate() only just as the page is losing visibility. This pattern could occur if the watch is going to sleep, or transitions between interface controllers.

Here's a trace of a page-based interface with 5 interface controllers.

We can see that the init() ➡ awakeWithContext() call pattern is the same, but as soon as page0 is activated, it immediately activates, then deactivates page1.

Common Issues

While Apple's caching works well for populating the contents of the UI, but most issues revolve around doing any non-view configuration work in willActivate(). In particular, detecting visibility state change of the WKInterfaceController.

Here's a few scenarios that are more difficult in WatchOS 1.0.1:

Asynchronous updates kicked off by willActivate(). For example, willActivate() kicks off some network calls to populate data, and when it returns, didDeactivate() has already been called, so the updates will fail.

Knowing which interface controller is visible. For example, your MMWormHole is reporting a change on your iOS app, and you'd like to call a function on only the visible interface controller to refresh UI.

In the calling patterns of 1.0 and 1.0.1, the visible controller always gets willActivate() called first. If we assume that this will always hold true, we can definitively know which controller is visible.

Each time willActivate() is called, track it in an activeControllers dictionary along with the timestamp. Remove it when didDeactivate() gets called. When we want to know which controller is the visible one, return the controller with the smallest timestamp.

With this strategy, it's extremely important to clear the activeControllers before you call reloadRootControllersWithNames or you might orphan a controller that you need. See resetControllers() for example.

The flaw with this strategy is in the assumption, that Apple will always create the visible controller first before caching the next one. The API documentation makes no guarantees on call order, so this solution is definitely hacky. It's a "safe hack" though, since logically Apple would likely want to start loading the first visible controller before trying to cache others. There are lots of reasons, including threading races, as to why this assumption could be broken in the future.

Visible Controller Solution 2: Poll until only one controller active.

If we dislike the above hack, another option is to wait until our activeControllers dictionary resolves down to exactly one element. It works the same as the Solution 1 code snippet with just visibleController() and refreshVisibleController() changed.

The problem with this strategy is that polling causes additional lags and is generally messy control flow every time you want to take an action on the visible controller. The above solution also requires additional code to be production ready. This code will poll forever if activeControllers is empty. (e.g. the watch went to sleep)

Final Thoughts

There is currently no way to detect if the watch is running WatchOS 1.0 or 1.0.1, so the code needs to be written in such a way that it works on all OS versions. It appears that Apple's intention is that willActivate() and didDeactivate() are intended just for setting up and tearing down views and no additional side effects should be occurring in the watch app. This is great in theory, but unfortunately there are no convenient method to actually know the difference between interface controller setup and the interface controller actually appearing.

The flow is really nice in iOS: init ➡ viewDidLoad ➡ viewWillAppear ➡ viewDidAppear and viewWillDisappear ➡ viewDidDisappear ➡ viewDidUnload ➡ dealloc. On the watch, perhaps this is overkill, and I can see them wanting to reduce communication over Bluetooth by reducing the number of events going over the air.

However the current state of the WatchKit API leads developers to a lot of hacks and workarounds when writing non-trivial code. Even if the watch UI is simple and clean, the underlying logic is sometimes necessarily complex to bring data from other sources cached and ready to go just-in-time.