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.
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 run time 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 keep track of the weather when you're thinking about flying a kite, so we'll need weather data-- especially wind. We'll use background url sessions to grab weather data for the latest cached location periodically. Flying kites with friends is great fun, so lastly 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. Anytime 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 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 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 anytime 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 currentTimelineEntry. In response, our app then builds and 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 TimelineEntry for the complication to update. A handler is provided to pass back the entry after it's built. We choose a template that's appropriate for the type of complication. We fill in the template with providers to build a timeline entry. Once the entry has been created, we 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.
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 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 apps 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 scheduledDate. If 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 when 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 scheduleDate 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 ExtensionDelegate, it also generates a method to handle background tasks. The system might have more than one task to complete. Our ExtensionDelegate loops through all tasks and handles each one. The generated code provides default handlers for all types of background tasks the app can receive.
We're handling a WKApplicationRefreshBackgroundTask and replaced the default handler with our own code. In this code, purely as an example, we retrieve the user info we added when we scheduled the request. We use the 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 tasks, so we must do 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 ExtensionDelegate, 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 complete 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 health data provider to refresh its data. It does that asynchronously. When it has finished refreshing its data, it calls the completion handler telling us whether or not to update the complications.
Within the completion handler, we optionally 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 network activity is allowed, you can 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 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 answer 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, our 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 your session when your app is launched so you can receive the URLSession delegate call backs. 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 and 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 a URLSession, we get a background configuration, we mark the configuration as non-discretionary, and make sure sessionSendsLaunchEvents is set to true, so the app is launched in the background. Then, we use that configuration to create the URL session. Since we set the delegate queue to nil, URLSessions responses will be sent on a background serial queue.
Continuing with our WeatherDataProvider, we've added a schedule method to create and schedule the download task. We can have multiple outstanding requests, but in this case, we 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 CoreLocation and create our download task for the background session. We set the earliestBeginDate for this task.
As we did earlier, we make our first request right away and set follow up requests for every fifteen minutes. We set the number of bytes we expect to send and receive. Finally, we resume the task so that it is ready to run. After it's scheduled, the download task will run independently of our app. When it's complete, our app will be resumed in the background or launched if necessary to handle the requests. When the download is complete, the WKExtension delivers a WKURLSessionRefreshBackgroundTask to our session 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 the didFinishDownloadingTo: delegate call. Regardless of whether the download completed successfully or not, our delegate will receive didCompleteWithOptionalError?: When that call is complete, 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 extension delegate is asked to handle the WKURLSessionRefreshBackgroundTask. The extension 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, if necessary, and then mark the task completed. Within our WeatherDataProvider, 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 download 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 didCompleteWith optional Error: We want to call the 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 WKURLSessionRefreshBackgroundTasks. As before, our extension delegate will be asked to handle these requests. We'll use our WeatherDataProvider to handle these requests. While the task is active, the WeatherDataProvider will receive delegate calls from the URLSession subsystem. willBeginDelayedRequest 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 CoreLocation 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 should make sure to set the task completed within fifteen 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 polling 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 our fellow kite flyers.
Servers can send up to fifty 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've 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, create 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's 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. Within our watch app, we'll use our PushNotificationProvider to register 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 the delegate. It also sets the desired push type to .complication which matches the .complication identifier and certificate we created and installed on our server. When registration is complete, the registry returns credentials in the didUpdate pushCredentials call.
We send these credentials to our server so that it 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's servers, which will then deliver them to the watch. As we said earlier, fifty of these are allowed per watch, per day. When sending a complication push, format it as you would any background push by 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 didReceiveIncomingPushWith:payload: A completion handler is provided for our app to 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 specified when registering with PushKit is used to make this call. In our case, that's the main queue. A completion handler is provided.
It must be called when the payload has been processed. We 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 want to update only the complications that need it. That's it for complication pushes. To wrap up, each instance of an app to receive up to fifty 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 with 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 fifty times per day. Space them at regular intervals or send them more frequently if their data are bursty. The fifty-first 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 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
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.