Streaming is available in most browsers,
and in the Developer app.
-
Keep your complications up to date
Time is of the essence: Discover how your Apple Watch complications can provide relevant information throughout the day and help people get the information they need, when they need it. Learn best practices for capitalizing on your app's runtime opportunities, incorporating APIs like background app refresh and URLSession, and implementing well-timed push notifications.
Resources
Related Videos
WWDC21
WWDC20
-
Download
Hello and welcome to WWDC.
Hello, I'm Mike Lamb. I'm a software engineer on the Apple Watch team. I'm here today to talk about the best ways to keep your complications up to date. We have a lot to cover, so let's get started. Complications are intrinsic to the user experience on the Apple Watch. With Face Sharing, SwiftUI complications and multiple complication APIs being released in watchOS 7, complications are an even bigger focus for Watch apps this year.
Complications are a great way to provide timely and relevant information on the Watch face at a quick glance. Keeping complications up to date is imperative to making a great experience for customers.
watchOS provides complication apps' special capabilities to enable a great experience.
Complication apps are kept around even when other apps may be stopped and removed from memory. If the system does need to stop a complication app, it will be restarted later to update its complications.
Since complications are always visible, complication apps are considered in use for privacy. Our goal today is to go over the techniques apps can use to keep dynamic complications up to date. We'll start by discussing an example app I'm building to illustrate these techniques.
An excellent time to update our complications is when the app is being actively used. We'll go over how to do this. Even when the app isn't in the foreground, watchOS provides mechanisms to allow it to keep its complications up to date. Background updates make complications feel like magic.
The first background update mechanism is Background App Refresh.
Background App Refresh allows apps to schedule background runtime to access APIs and data on the Watch. Apps can also schedule background URLSessions to pull data from servers while the app isn't active. And data can be sent directly to the app on the Watch using complication pushes. We'll end today's talk with an example of how to do this.
To reiterate, for each of the three background mechanisms, the app doesn't need to be active all the time. It will be launched as necessary to update its complications.
All these mechanisms can be used separately or in combination depending on your app's needs. To illustrate how to keep complications up to date, I've been working on an example kite flying app. Our goal is to provide a compelling experience without requiring a phone, so we're building an independent Watch app. We're gonna make use of the new APIs available in watchOS 7 to support multiple active complications.
To be clear, today we're showing how to keep complications up to date. We're not showing how to design or build them. See our other sessions for information on how to do that. We want the app to encourage people to be more active, so our first complication will pull information about the day's activity from HealthKit. We'll use Background App Refresh to pull that data. It's important to track the weather when you're thinking about flying a kite, so we'll need weather data, especially wind. We'll use background URLSessions to grab weather data for the latest cached location periodically. Flying kites with friends is great fun, so we'll want to provide a complication that displays encouragement from our friends. We'll use complication pushes to provide updates for this complication. Let's talk about updating in the foreground first. Any time our app is launched by the user is a great time to update our complications. Once we have updates we want to display, we use the ClockKit APIs to reload our complication's timeline.
A complication consists of a timeline of entries. When an app wants to update a complication, it calls reloadTimeline on the complication's server for that complication.
Here we've written an updateActiveComplications method which we'll use throughout our app.
This method iterates over the array of active complications and asks the complication's server to reload each one.
In this example, we're updating all complications each time.
Typically, you should be more selective and only update those complications that need to be updated. We will call updateActiveComplications any time we want to update our complications.
After reloadTimeline is called by updateActiveComplications, or at other times as needed, the complication server will call the app's CLKComplicationDataSource to get the current timeline entry.
In response, our app then builds an entry using a template and providers.
When the app is finished building the entry, our app passes it to the complication server using the provided completion handler.
Here's the code. After reloadTimeline is called by updateActiveComplications, our CLKComplicationDataSource is invoked.
It is asked for the current timeline entry for the complication to update.
A handler is provided to pass back the entry after it's built.
We use a template that's appropriate for the type of complication. We fill the template with providers to build a timeline entry. Once the entry has been created, you pass it back using the handler that was provided.
As you can see, updating complications while in the foreground is straightforward.
We ask the complication server to reload our complications and then provide the current entry for each one.
We'll do this whenever the user changes selections in the app or our app receives new data while it's in the foreground.
Often though, our app isn't in the foreground, and in that case we can use mechanisms like Background App Refresh to get the data needed to refresh our complications.
Going back to our example app, we want to pull data from HealthKit for our activity complication.
Background App Refresh allows us to schedule periodic updates to keep that complication up to date even when the app isn't in use.
Our app can use Background App Refresh to refresh complications up to four times per hour.
This number doesn't change regardless of how many of our app's complications might be configured on the active Watch face.
The actual number of updates an app receives is dependent on conditions such as how many other processes are running and battery usage.
To schedule Background App Refresh, call scheduleBackgroundRefresh on WKExtension. It's possible your app will be launched in the background, so consider scheduling your first request in applicationDidFinishLaunching.
We've written a method to schedule Background App Refresh. First we choose the schedule date. This is our first request. We make that request sooner as shown. The system will choose the right time to launch your app. This will always be after the time you've requested. Typically, it is within a minute or two, but that depends on system conditions. Use the userInfo dictionary to supply your own data. In this case, just to illustrate how this is done, we're passing the time the request was made.
Once we have the schedule date and the optional userInfo, we call scheduleBackgroundRefresh on WKExtension.
WKExtension will call our completion handler asynchronously on the main thread when the request has been scheduled. Process any errors that might have occurred.
When the task is ready, our app will be made active, and the extension delegate will be called to handle the background task.
After doing some processing, we'll request a complication update and schedule our next Background App Refresh. Then we'll set the task completed.
Let's look at our extension delegate.
When Xcode generates our extension delegate, it also generates a method to handle background tasks.
The system might have more than one task to complete. Our extension delegate loops through all tasks and hails each one.
Generated code provides default handlers for all types of background tasks the app can receive.
We're handling it WKApplicationRefreshBackgroundTask and replacing the default handler with our own code.
In this code, purely as an example, we retrieve the userInfo we added when we scheduled the request. We use a date that we stored within it to calculate the time since the request was made.
Then we call our updateActiveComplications method and ask the complication server to reload our active complications.
We then schedule the next background refresh.
After we've updated our complications and scheduled another request, then we complete the current task.
We pass "false" to indicate that no snapshot is needed. Each complication update results in a snapshot request, so we don't have to request one separately.
The app may be suspended as soon as it completes the background task, so we must do all our work before setting the task completed.
If we want to do something more complex, like access HealthKit, we'll need to revise our strategy.
To avoid putting too much code in our extension delegate, I've added a data provider that uses HealthKit and adds a method that takes its own completion handler.
HealthKit queries can be asynchronous, so we'll need to wait until the refresh is completed before updating our complications, scheduling our next request and setting the task complete.
Doing this is pretty straightforward because the HealthKit work is done by our new HealthDataProvider.
We call the HealthDataProvider to refresh this data. It does that asynchronously.
When it's finished refreshing its data, it calls the completion handler, telling us whether or not to update the complications.
Within the completion handler, we actually update our complications, schedule our next refresh and set the task completed.
To review, Background App Refresh is great for scheduling periodic background tasks. Your app can be resumed or launched to handle these tasks up to four times per hour.
Here are some guidelines to keep in mind.
Only one request is outstanding at a time. If you need periodic updates, do what I've shown and schedule the next one before marking the current one complete.
No networking activity is allowed. You use most of the APIs available on the Watch, but URLSession is an exception. If you do try to use URLSession, it will fail with an error.
Your app is limited to a maximum of four seconds of active CPU time. That may not sound like much, but four seconds of constant processing is actually quite a lot. If you need to do more processing than that, consider breaking it into smaller chunks.
The app has a maximum of 15 seconds of total time to complete the task. A common reason for going over 15 seconds is neglecting to mark the task completed.
Background App Refresh is great, but if you need to access data over a network while in the background, use a background URLSession. Background URLSessions allow your app to schedule and receive data even when the app isn't running.
Background URLSession can be used in addition to Background App Refresh. You can even change the request on the fly and insert authentication challenges. It's pretty cool.
We'll use it to retrieve weather information for the localized wind complication we're planning to add to our app.
Under most conditions, your app can make and receive up to four requests per hour. The actual number depends on a number of factors, including the availability of Wi-Fi, cellular reception and battery life.
Your app can have multiple outstanding background download tasks. Always make sure to attach to your session when your app is launched so you can receive the URLSession delegate callbacks.
First, let's go over how to schedule background URLSessions.
In our app, we want to get weather data periodically, and we'll use the URLSession framework for this purpose. We've created a WeatherDataProvider to be our URLSession delegate. Our data provider creates a background URLSession configuration. We set sessionSendsLaunchEvents on the background configuration to wake the app in the background.
We use the configuration to create a URLSession in a download task. We set the task's earliest begin date. That will be the date the task is scheduled for. Then we resume the task to start it. Here's the first part of our WeatherDataProvider. It's a URLSessionDownloadDelegate.
To create our URLSession, we get a background configuration, we mark the configuration as nondiscretionary and make sure sessionSendsLaunchEvents is set to "true" so the app is launched in the background. Then we use a configuration to create the URLSession. So, if we set the delegateQueue to "nil," URLSession's responses will be sent on a background serial queue. Continuing with our WeatherDataProvider, we've added a schedule method to create and schedule a download task. We have multiple outstanding requests, but in this case, you only need one, so we'll schedule a new background task as long as one isn't already outstanding. We build the URL we need using the latest cached location from Core Location and create our download task for the background session. We set the earliest begin date for this task. As we did earlier, we make our first request right away and schedule follow-on requests for every 15 minutes. We set the number of bytes we expect to send and receive. Finally, we resume the task that is ready to run. After it's scheduled, the download task will run independently of our app. When it's completed, our app will be resumed in the background or launched as necessary to handle the request.
When the download is complete, the WKExtension delivers a WKURLSessionRefreshBackgroundTask to our extension delegate.
We use our WeatherDataProvider to handle the request.
Until we mark the task completed, URLSession delegate methods will be delivered. Don't mark the task as completed before handling those calls.
If the task completed successfully, our delegate receives a didFinishDownloadingTo delegate call.
Regardless of whether the download completed successfully or not, our delegate will receive didCompleteWithOptionalError. When that call is completed, the WeatherDataProvider calls the completion handler it was given, which allows us to update our complications, schedule a new request and then set the task completed.
The download task we scheduled earlier is complete, and now it's time to handle the results.
Our session delegate is asked to handle WKURLSessionRefreshBackgroundTask. The session delegate asks the WeatherDataProvider to refresh and passes a closure to be called when done.
When the refresh is done, the WeatherDataProvider calls the closure. Within that closure, we schedule our next retrieval, update complications as necessary and then mark the task completed. Within our WeatherDateProvider, our refresh method stores the completion handler it is passed so it can call it once the expected delegate methods have been delivered. Our WeatherDataProvider, since the task is completed, receives the URLSession delegate method downloadTask didFinishDownloadingTo.
The data we asked for has been downloaded to a file, we check for that, and then process the json weather data we received.
After the download task is complete and we've processed the data, our app receives didCompleteWithOptionalError. We want to call a completion handler on the main queue, so we dispatch to the main queue and call the completion handler. If there's no error, we tell it to update the complications. We then set the completion handler to "nil" so it isn't called more than once.
Depending on the request, our app may get intermediate requests that allow our app to update the URL, cancel the download task or answer authentication challenges before the download completes.
Let's look at those briefly. As in other cases we've looked at so far involving URLSession tasks, these will come to our app as WKURLSessionRefreshBackgroundTask.
As before, our extension delegate will be asked to handle these tasks.
We'll use our WeatherDataProvider to handle these requests.
While the task is active, the WeatherDataProvider will receive delegate calls from the URLSession subsystem.
WillBeginDelayRequest allows our app to update or cancel our URL request. For example, if it's been a long time since we initiated our request, we might substitute the latest cached location from Core Location for the one we specified when we first made the request.
DidReceiveChallenge allows our app to respond to any authentication challenges that may occur. Our delegate will receive SessionDidFinishEvents when all events have been delivered, so this is when we'll call our completion handler.
You might be tempted to schedule a new task at this point, but don't, as the current task hasn't been fully completed.
Instead, just mark this task as completed. The guidelines are the same as for Background App Refresh.
Your app should avoid expensive processing, and make sure to set the task completed within 15 seconds of when it was received. Background URLSessions are great for retrieving data from remote servers. They can be scheduled, revised or canceled as needed.
The last mechanism we'll look at today are complication pushes. Depending on the use case, pushes can be more efficient than pulling servers for data.
This is especially true for event-driven data. We'll use complication pushes to provide data for our last complication, which keeps track of encouragement from fellow kite flyers. Servers can send up to 50 complication pushes per day to each individual Watch. Complication pushes don't need to be regularly spaced. If the data are bursty, requests can be sent more rapidly than with the other mechanisms we discussed. Throttling may be necessary to prevent exceeding the daily cap.
The server sending the pushes to the Watch needs to have a proper certificate, so let's take a quick look at how to set that up. First we need an identifier that includes your app's bundle ID and ends in .watchkitapp.complication. This is crucial. If the app identifier isn't in the correct format, your push may be rejected by Apple servers or may not be received on the Watch.
Once the .complication app identifier has been created, use that to create an Apple Push Notification service SSL Certificate. Your server will use that certificate to authenticate to Apple's push notification servers.
Your app also needs the remote notification background mode and the push notifications capabilities in its WatchKit extension.
With our Watch app, we'll use our PushNotificationProvider to register it with PushKit.
When the registration succeeds, the app receives credentials. Upload these credentials to your server so your server can communicate with the Watch.
Here's our PushNotificationProvider. It is a PKPushRegistryDelegate. It creates a PKPushRegistry instance and provides the main queue for callbacks.
It sets itself as a delegate.
It also sets the desiredPushType to .complication, which matches the .complication identifier and certificate we created and installed on our server.
When registration is complete, registry returns credentials and the didUpdate pushCredentials call. We send these credentials to our server so they can communicate with this instance of the app.
Once the Watch uploads its credentials to the server, the server uses those credentials to send pushes for that Watch to Apple servers, which will then deliver them to the Watch. As we said earlier, 50 of these are allowed per Watch per day.
When sending a complication push, format as you would any background push providing the content-available entry in the aps dictionary. After the push is received by PushKit on the Watch...
it is passed to our app with didReceiveIncomingPushWithPayload.
A completion handler is provided for after call when it has finished processing the push.
Back to the implementation of our PushNotificationProvider. When a push is available, our app will be resumed or launched, if it's not active.
At that time, the didReceiveIncomingPush payload for type will be called on our delegate. The queue we specify when registering with PushKit is used to make this call.
In our case, that's the main queue.
The completion handler is provided. It must be called when the payload has been processed. Process the payload and call our extension delegate to update our complications. Here we're updating all complications. If this were a shipping app, we'd want to update only the complications that need it.
That's it for complication pushes. To wrap up, each instance of an app can receive up to 50 per day. That number doesn't change when an app has multiple active complications.
The guidelines are the same as they are for other types of background updates. To summarize the techniques we discussed today, use foreground opportunities, when active, to update your complications. Do this especially when the app's state changes in response to input or if your app pulls data from a server while it's in the foreground.
Apps can use a ProcessInfo activity to complete work when your app moves from the foreground to the background.
Background App Refresh is great to schedule runtime for accessing your own data or using the watchOS APIs such as HealthKit.
Apps can use background refresh to update its complications up to four times per hour.
Background URLSession tasks can be scheduled to pull data from servers up to four times per hour. Remember to always reattach your delegate when your app is activated so that it can receive updates that might be pending.
Push notifications can be sent from your servers to each Watch up to 50 times per day. Space them at regular intervals or send them more frequently if the data are bursty. The 51st and later notifications will be ignored, so apply throttling on the server as needed. Use these mechanisms individually or combine them as needed. You can see how important it is to keep your complications up to date. There are multiple mechanisms at your disposal to allow you to keep your complications up to date even when the app isn't active. Mix and match the techniques we've discussed to deliver the best possible experience. All the mechanisms we discussed today can be used with independent Watch apps to give your app the greatest possible flexibility. The "Meet Watch Face Sharing" video from earlier this week discusses the new multiple complication APIs in greater detail.
"Build Complications in SwiftUI" describes how to build beautiful complications using SwiftUI.
Lastly, there's a great video on "Creating Independent Watch Apps" from last year. Check all of these out. Thank you, and I can't wait to see the magical complications you build using these techniques. Have a great WWDC.
-
-
3:32 - updateActiveComplications
class ExtensionDelegate: NSObject, WKExtensionDelegate { func updateActiveComplications() { let complicationServer = CLKComplicationServer.sharedInstance() if let activeComplications = complicationServer.activeComplications { for complication in activeComplications { complicationServer.reloadTimeline(for: complication) } } } }
-
4:26 - getCurrentTimelineEntry
class ComplicationController: NSObject, CLKComplicationDataSource { func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { switch (complication.family) { case .modularSmall: let template = CLKComplicationTemplateModularLargeTallBody.init( headerTextProvider: headerTextProvider, bodyTextProvider: bodyTextProvider) entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template) } handler(entry) } }
-
6:06 - scheduleBar
private func scheduleBAR(_ first: Bool) { let now = Date() let scheduledDate = now.addingTimeInterval(first ? 60 : 15*60) let info:NSDictionary = [“submissionDate”:now] let wkExt = WKExtension.shared() wkExt.scheduleBackgroundRefresh(withPreferredDate: scheduledDate, userInfo:info) { (error: Error?) in if (error != nil) { print("background refresh could not be scheduled \(error.debugDescription)") } } }
-
7:08 - handleBAR
class ExtensionDelegate: NSObject, WKExtensionDelegate { func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) { for task in backgroundTasks { switch task { case let backgroundTask as WKApplicationRefreshBackgroundTask: if let userInfo:NSDictionary = backgroundTask.userInfo as? NSDictionary { if let then:Date = userInfo["submissionDate"] as! Date { let interval = Date.init().timeIntervalSince(then) print("interval since request was made \(interval)") } } self.updateActiveComplications() self.scheduleBAR(first: false) backgroundTask.setTaskCompletedWithSnapshot(false)
-
8:47 - handleBAR (DataProvider)
class ExtensionDelegate: NSObject, WKExtensionDelegate { var healthDataProvider: HealthDataProvider func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) { for task in backgroundTasks { switch task { case let backgroundTask as WKApplicationRefreshBackgroundTask: healthDataProvider.refresh() { (update: Bool) -> Void in if update { self.updateActiveComplications() } self.scheduleBAR(first: false) backgroundTask.setTaskCompletedWithSnapshot(false) }
-
11:35 - Instantiate backgroundURLSession
class WeatherDataProvider : NSObject, URLSessionDownloadDelegate { private lazy var backgroundURLSession: URLSession = { let config = URLSessionConfiguration.background(withIdentifier: “BackgroundWeather") config.isDiscretionary = false config.sessionSendsLaunchEvents = true return URLSession(configuration: config, delegate: self, delegateQueue: nil) }()
-
12:02 - Schedule backgroundURLSessionTask
func schedule(_ first: Bool) { if backgroundTask == nil { if let url = self.currentWeatherURLForLocation(delegate.currentLocationCoordinate) { let bgTask = backgroundURLSession.downloadTask(with: url) bgTask.earliestBeginDate = Date().addingTimeInterval(first ? 60 : 15*60) bgTask.countOfBytesClientExpectsToSend = 200 bgTask.countOfBytesClientExpectsToReceive = 1024 bgTask.resume() backgroundTask = bgTask } } } }
-
13:29 - handle backgroundURLSession
class ExtensionDelegate: NSObject, WKExtensionDelegate { var weatherDataProvider:WeatherDataProvider func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) { for task in backgroundTasks { switch task { case let urlSessionTask as WKURLSessionRefreshBackgroundTask: weatherDataProvider.refresh() { (update: Bool) -> Void in weatherDataProvider.schedule(first: false) if update { self.updateActiveComplications() } urlSessionTask.setTaskCompletedWithSnapshot(false) }
-
13:59 - handle backgroundURLSession
class WeatherDataProvider : NSObject, URLSessionDownloadDelegate { var completionHandler : ((_ update: Bool) -> Void)? func refresh(_ completionHandler: @escaping (_ update: Bool) -> Void) { self.completionHandler = completionHandler }
-
14:08 - didFinishDownloadingTo
class WeatherDataProvider : NSObject, URLSessionDownloadDelegate { func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { if location.isFileURL { do { let jsonData = try Data(contentsOf: location) if let kiteFlyingWeather = KiteFlyingWeather(jsonData) { // Process weather data here. } } catch let error as NSError { print("could not read data from \(location)") } } }
-
14:23 - didComplete
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { print("session didCompleteWithError \(error.debugDescription)”) DispatchQueue.main.async { self.completionHandler?(error == nil) self.completionHandler = nil } } }
-
17:53 - Complication Pushes
class PushNotificationProvider : NSObject, PKPushRegistryDelegate { func startPushKit() -> Void { let pushRegistry = PKPushRegistry(queue: .main) pushRegistry.delegate = self pushRegistry.desiredPushTypes = [.complication] } func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { // Send credentials to server } func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { // Process payload delegate.updateActiveComplications() completion() }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.