Now that your complications are available to place on the watch face, you need to address one more thing.
How do you make sure the displayed data is up to date?
There are four options available to you:
- Update based on changes while the app is active.
- Schedule background tasks to make changes.
- Schedule background
URLSessiondownloads. - Send notifications via
PushKit.
You’ve already learned how to do that first one, by reloading a timeline. So over the next three episodes, we’re going to talk about those last three techniques. First up: Background Tasks
Scheduled background tasks
There will be times when you know an update should take place in the future, but the watch likely won’t be running your app during that time.
-
Calling
scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:)fromWKExtensionlets you specify a future date when watchOS should wake your app up in the background and perform work. -
When watchOS starts the background task, your app gets four seconds of CPU time and 15 seconds of total time to complete the task.
-
You can schedule up to four background tasks per hour.
-
But you can only have one task scheduled at a time. If you schedule a second task while one is already scheduled, the previous task will cancel automatically.
ExtensionDelegate.swift
We’re starting in a new project to help us examine our update options.
It is aptly named “Updates” In this new project, open ExtensionDelegate.swift.
When watchOS launches your app to perform a background task, it calls the handle(_:) method from WKExtensionDelegate.
So, let’s set that up here in ExtensionDelegate
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
}
watchOS provides you with one or more tasks, so you need to iterate through them.
backgroundTasks.forEach { task in
}
WKRefreshBackgroundTask is a base class, so we’ll need to check for a specific subclass.
backgroundTasks.forEach { task in
switch task {
}
}
But if the task type isn’t one you care about, mark the task as completed and pass in false so that a new snapshot isn’t scheduled since you haven’t performed any changes.
backgroundTasks.forEach { task in
switch task {
🟩default:
task.setTaskCompletedWithSnapshot(false)
}
}
The subclass we are interested in is WK Application Refresh Background Task
switch task {
🟩case let task as WKApplicationRefreshBackgroundTask:
default:...
There are four steps involved when a background task launches:
- Perform the necessary work to complete the task.
- Update your complications if something has changed based on the task.
- Schedule the next background task, if required.
- Mark the task as completed.
switch task {
case let task as WKApplicationRefreshBackgroundTask:
🟩// Perform work
// Update complication, if needed
// Schedule next task
// Mark task completed
default:...
We already know how to update the complication, and mark a task as complete. To handle the rest, we’ll create a new BackgroundWorker class.
The background worker
So, add a new file, called BackgroundWorker.swift Import WatchKit at the top, along with Foundation.
import Foundation
import WatchKit
And create that class. It’ll be final, because we don’t want to subclass it.
final class BackgroundWorker { }
First up, a method to perform the necessary background work.
When ExtensionDelegate is ready to run your scheduled job, it’ll call this perform(_:) method.
Handle all the required work, then call the completion handler with true if the active complications should update with new values, otherwise false.
public func perform(_ completion: (Bool) -> Void) {
// Do your background work here
completion(true)
}
Your worker also needs to be able to schedule jobs:
public func schedule() { }
If the app is just starting, you might need to schedule a first background job immediately.
public func schedule(🟩firstTime: Bool = false) {
If it’s the first run, then start a minute from now, otherwise start 15 minutes later.
let minutes = firstTime ? 1 : 15
Remember, you only get four updates an hour. So you need to wait at least 15 minutes for subsequent calls if you plan to spread the calls over the hour.
Then to figure out when the task should be scheduled, add the number of minutes to the current time.
let when = Calendar.current.date(
byAdding: .minute,
value: minutes,
to: Date.now
)!
Finally, schedule the job to run at the desired time with that scheduleBackgroundRefresh method I mentioned earlier. If you need data to be available to the job when watchOS launches it, use the userInfo parameter. And wrap it up by handling any errors.
WKExtension
.shared()
.scheduleBackgroundRefresh(
withPreferredDate: when,
userInfo: nil
) { error in
if let error = error {
print("Unable to schedule: \(error.localizedDescription)")
}
}
}
}
ExtensionDelegate background task
Switch back to ExtensionDelegate.swift and add a new backgroundWorker property to ExtensionDelegate:
private let backgroundWorker = BackgroundWorker()
Now we can finish up the switch statement!
Call the perform method and supply the completion handler to call when the work finishes.
backgroundWorker.perform { updateComplications in
}
Inside of the closure, if you passed true to the completion handler, tell the complications to update themselves.
if updateComplications {
Self.updateActiveComplications()
}
And finally schedule the next background task, and mark the task as completed.
backgroundWorker.schedule()
task.setTaskCompletedWithSnapshot(false)
}
Let me point out a few important details about what we’ve done, here.
Note: Pay special attention to the fact that you need to schedule the next background task before marking the current task as complete.
watchOSwill stop providing cycles to your app once you specify the task is done.
Also, notice how you mark the task as completed inside of the completion handler. handle(_:) will complete before your job finishes. If you mistakenly mark the task as complete outside of the completion handler, your job will never fully run because watchOS will terminate it.
And finally,
Note: If your complications are updated, watchOS will schedule a snapshot automatically. And, if they aren’t you don’t need a new snapshot. So, always pass
falsetotask.setTaskCompletedWithSnapshot.
Depending on your app’s requirements, it may not make sense to automatically schedule the next task.
The pattern we’ve used here assumes that you’ll call schedule(true) from somewhere like applicationDidFinishLaunching and need to repeat on a known time cycle.
While most frameworks are available to your app during a background task, the notable exception is URL downloads.
If you try to perform a URL download from a background task, watchOS will hand you an error.