The most critical time for a watch app’s performance is when it launches. While a watch app is loading, the system displays the loading indicator, shown here, and finally replaces it with the app’s initial user interface. If an app takes too long to launch, the screen will go to sleep before it’s even done, and the user may well forget they even launched it. It would be a pity if the loading indicator was the only part of your app that your users saw! To avoid this misfortune, let’s look at each thing the app does when it starts and try to make it happen faster.
When a watch app loads, the interface controller marked as the initial interface controller is loaded and presented to the user. It needs to complete loading before the user can interact with it. All of the interface controller lifecycle methods need to finish: init, awakeWithContext(_:), and willActivate. You can test this yourself by adding logging to these methods; the logs will appear before the app loads.
It’s easy for me to tell you to make these methods fast. The trouble is how? There are some watch-specific things you can do to make these methods faster. For instance, you can reduce the amount of data loaded by the storyboard. If your storyboard has a 10-megapixel image inside a button, the system has to load the entire image, shrink it down to the appropriate size, and then render it onscreen. If you use an appropriately sized image, the watch only has to load the image and render it.
Not only does the system load the initial interface controller at app launch time; it will also load any interface connected to it using a “next page” segue. If you have five interface controllers linked using “next page” segues, the first of which is marked as the initial interface controller, the system will call init and awakeWithContext(_:) on each interface controller in turn. This compounds the performance implications of loading each interface controller. For instance, if they have tables with many items in them and the tables are initialized in awakeWithContext(_:), all of that initialization has to happen for each interface controller, even if the user will never navigate to some of them.
You might think that the solution to this problem is to avoid doing complicated initialization in init and awakeWithContext(_:) in favor of using willActivate, and you’d be partially correct. Moving data-intensive tasks like loading data into tables into willActivate can help delay when the data is loaded. In the case of app startup, there’s one important detail when it comes to paged interface controllers. Consider the following layout of interface controllers:
This sample app has four interface controllers, named InterfaceControllerA through InterfaceControllerD. They’re all loaded at app startup as a group of paged interface controllers, and I’ve added logging to them to see the sequence of events at app startup. Here’s the output from those logs:
| InterfaceControllerA.init() |
| InterfaceControllerA.awakeWithContext(_:) |
| InterfaceControllerB.init() |
| InterfaceControllerB.awakeWithContext(_:) |
| InterfaceControllerC.init() |
| InterfaceControllerC.awakeWithContext(_:) |
| InterfaceControllerD.init() |
| InterfaceControllerD.awakeWithContext(_:) |
| InterfaceControllerA.willActivate() |
| InterfaceControllerB.willActivate() |
| InterfaceControllerB.didDeactivate() |
Everything seems fine at first: the interface controllers are initialized with both init and awakeWithContext(_:), and then InterfaceControllerA receives the willActivate method. But what happens next is interesting: InterfaceControllerB receives willActivate immediately followed by didDeactivate. Why is that? If you swipe from one to the next, you’ll see why. Here’s the log output from swiping to the second interface controller:
| InterfaceControllerB.willActivate() |
| InterfaceControllerA.didDeactivate() |
| InterfaceControllerC.willActivate() |
| InterfaceControllerC.didDeactivate() |
As you swipe, InterfaceControllerB receives willActivate, and InterfaceControllerA receives didDeactivate. That seems fair and just, because you’re leaving A to arrive at B. Next, you see the same oddity: InterfaceControllerC receives the willActivate and didDeactivate methods right after one another. Why is this? The answer is a performance trick on watchOS’s part. By calling willActivate on the interface controller, any work you need to do before the user swipes to an interface controller can be performed, and then you return to an inactive state in didDeactivate. When the user swipes to the next interface controller, anything in willActivate that needs to happen only once has already happened, so the swipe happens as quickly as possible. It’s a performance trick that you should be aware of at app startup as well.
The initial interface controller in a watch app’s storyboard isn’t the only thing that needs to load before an app starts. The extension delegate lifecycle methods are also called before the loading indicator disappears, so it’s paramount to return from them immediately. You can see the sequence of events by running a sample app with logging added. The app I have been referencing is available in the book’s source code download, at code/Chapter 12/SlowLauncher. Running it shows that after the interface controllers initialize, two lifecycle methods fire: applicationDidFinishLaunching followed by applicationDidBecomeActive.
There’s nothing special to these methods in terms of performance, but there is an important caveat with applicationDidFinishLaunching. In that method, you can call WKInterfaceController’s reloadRootControllersWithNames(_:contexts:) method to load initial interface controllers other than those specified in the storyboard. However, this method happens after the storyboard’s interface controllers are loaded, so if your storyboard has a complicated interface controller as its initial interface controller, and then you load another complicated interface controller in the extension delegate, you’re wasting those resources. You can’t just leave the initial interface controller unset in the storyboard, because this results in a build error. If you’d rather just load your initial interface controllers in the extension delegate, one trick to increase performance is to set the initial interface controller in your storyboard to an empty interface controller. At least you know it’ll load quickly!
These methods can help the app start up more quickly and reduce the amount of time users see the dreaded loading indicator. App startup isn’t the only time performance matters; there are things you can do throughout the app to increase responsiveness. Let’s look at those next.
13.59.106.174