The lifecycle methods of WKExtensionDelegate and UIApplication (on iOS) protocols are similar, though they are fewer. The most common one to implement is applicationDidFinishLaunching, allowing you to do setup when the app is launched. The other two have you covered when the app is interrupted—for example, if the user gets a call in the middle of a run. First, applicationWillResignActive will be called on your delegate, allowing you to save your work, and then applicationDidBecomeActive lets you restore it once the app is back in the foreground.
The WatchKit extension lifecycle methods are shown here in the context of launching a WatchKit app. You see the state of the app from before it’s launched to when it’s running and all of the methods watchOS calls on its extension delegate in the process in the figure.
The first of these methods, applicationDidFinishLaunching, serves a very important role in WatchKit: it allows you to control the interface controller that the user is presented with when she starts the app. If you do nothing, then watchOS will load your storyboard and present whichever interface controller is marked as the initial interface controller. That isn’t always what you want, however. If, for instance, your user is already running but the app stops—perhaps it crashed (however unlikely) or the user went to do something with the music app that caused watchOS to suspend TapALap—then you wouldn’t want to start that user back at the Go Running screen. Instead, you’d want her to be taken directly to the run timer so she can continue her run. Ideally, all of her data will still be present and she’ll be able to continue the run. In fact, the user may not even notice that the app has restarted! Your goal should be to make the app launch seamlessly, regardless of how the user left it.
To facilitate this experience, you’re going to need to save the run data as you go. A great time to do that is when you mark the start date and begin timing the run. Before you write the code, however, let’s consider your options for saving the data. You could use HealthKit, but that would be a lot of code for your first version of the app. (We’ll cover using HealthKit in a later chapter.) You could create an SQLite database, but you really only need to save a few pieces of data. For cases like this, just like on iOS, NSUserDefaults can be a great way to store small key-value pairs of data. You’ll use that, saving your run’s startDate and lapTimes values to it. You’ll do this in RunTimerInterfaceController.swift, so let’s head there and get to work! Let’s do some refactoring to willActivate to handle the case where a run has already been started:
| override func willActivate() { |
| super.willActivate() |
| |
| if lapTimes == nil || startDate == nil { |
| let userDefaults = NSUserDefaults.standardUserDefaults() |
| |
| if let date = userDefaults.objectForKey("StartDate") as? NSDate, |
| times = userDefaults.objectForKey("LapTimes") as? [NSTimeInterval] { |
| startDate = date |
| lapTimes = times |
| } |
| else { |
| startRun() |
| } |
| } |
| |
| updateDistanceLabel() |
| } |
| |
| func startRun() { |
| lapTimes = [] |
| startDate = NSDate() |
| |
| let userDefaults = NSUserDefaults.standardUserDefaults() |
| userDefaults.setObject(startDate, forKey: "StartDate") |
| userDefaults.setObject(lapTimes, forKey: "LapTimes") |
| userDefaults.synchronize() |
| } |
When this code runs, if the interface controller doesn’t have an existing run, it’ll first look to see if there’s one stored in the user defaults. If not, then it’ll call startRun to create one. Next, you’ll need to update the saved lapTimes array in the user defaults at the end of every lap:
| @IBAction func finishLapButtonPressed() { |
| let lapFinishTime = NSDate() |
| |
| guard let startDate = startDate, lapTimes = lapTimes else { return } |
| |
| let totalRunDuration = lapFinishTime.timeIntervalSinceDate(startDate) |
| |
| let cumulativeLapDuration = lapTimes.reduce(0, combine: { $0 + $1 }) |
| |
| let lapDuration = totalRunDuration - cumulativeLapDuration |
| |
| self.lapTimes?.append(lapDuration) |
| |
| updateDistanceLabel() |
| |
| let userDefaults = NSUserDefaults.standardUserDefaults() |
| userDefaults.setObject(lapTimes, forKey: "LapTimes") |
| userDefaults.synchronize() |
| } |
There you have it. Your run will now resume automatically when returning to the run timer. To see this in action, force-quit the app during a run, and then restart it and tap Start Run again; this will open the run timer and display your still-in-progress run. You’re halfway there; next, let’s tell the app to open directly to the run timer when starting if there’s a run started.
Force-quitting an app is a great tool to add to your troubleshooting tool belt, but it’s not the most discoverable process. When the app is running, hold down the side button on your Apple Watch (not the Digital Crown but the button next to it) until the screen with the Power Off button appears, as you see here. Release the button; then press and hold the same button again. You’ll see the app animate away; it’s now been force-quit.
Open ExtensionDelegate.swift in the WatchKit extension and add some code to applicationDidFinishLaunching to handle runs in progress:
| func applicationDidFinishLaunching() { |
| returnToInProgressRun() |
| } |
| |
| func returnToInProgressRun() { |
| let userDefaults = NSUserDefaults.standardUserDefaults() |
| |
| if let _ = userDefaults.objectForKey("LapTimes") as? [NSTimeInterval], |
| _ = userDefaults.objectForKey("StartDate") as? NSDate { |
| WKInterfaceController.reloadRootControllersWithNames( |
| ["RunTimer"], contexts: nil) |
| } |
| } |
As you can see, you don’t need the values of the start date or lap times here, so you use the underscore to discard their values. It’s only important to this code that the values exist as the correct types. Build and run the app, and you’ll see that if there’s a run started, the app opens directly to the run timer, without ever loading the Go Running interface controller. Perfect!
If you tap Finish Run and then restart the app, you’ll see what you need to do next: clear out the data when a run is finished. This is a pretty simple addition to the endRun method of RunTimerInterfaceController:
| func endRun() { |
| let names = ["GoRunning", "RunLog"] |
| |
| let contexts: [AnyObject]? |
| |
| if let lapTimes = lapTimes, startDate = startDate { |
| let distance = track.lapDistance * Double(lapTimes.count) |
| |
| let run = Run(distance: distance, |
| laps: lapTimes, |
| startDate: startDate) |
| |
| let userDefaults = NSUserDefaults.standardUserDefaults() |
| userDefaults.removeObjectForKey("LapTimes") |
| userDefaults.removeObjectForKey("StartDate") |
| userDefaults.synchronize() |
| |
| contexts = [NSNull(), run] |
| } |
| else { |
| contexts = nil |
| } |
| |
| WKInterfaceController.reloadRootControllersWithNames(names, |
| contexts: contexts) |
| } |
Now that you’re deleting these values once you’ve created a Run instance out of them, the app will only reopen to a run before it’s finished. Try it out!
As you’ve seen, the lifecycle methods of WKExtensionDelegate allow you to customize your app’s behavior at launch and other important times when your interface controllers’ code alone wouldn’t be enough for a seamless user experience. They also allow you some more control over the coupling in your app; your interface code can stay in interface controllers, while your extension lifecycle code has its own place to exist. The extension delegate is good for more than lifecycle methods, however. Next, let’s look at some system APIs you can integrate with in your extension delegate.
3.22.217.193