After working with complications for a few days, I feel confident saying the following about the update process for updates that happen at a prescribed interval:
- The system calls
requestedUpdateDidBegin()
- This is where you can determine if your data has changed. If it hasn't, your app doesn't have to do anything. If your data has changed, you need to call either:
reloadTimelineForComplication
if all your data needs to be reset.
extendTimelineForComplication
if you only need to add new items to the end of the complication timeline.
- Note: the system may actually call
requestedUpdateBudgetExhausted()
instead of requestedUpdateDidBegin()
if you've spent too much of your complication's time budget for the day. This is the reason for this question.
- If you called
reloadTimelineForComplication
, the system will call getCurrentTimelineEntryForComplication
(along with the future and past variants that get arrays, depending on your time travel settings)
- This is conjecture as I haven't tested it yet, but I believe if you called
extendTimelineForComplication
that only the getTimelineEntriesForComplication(... afterDate date: NSDate ...)
would be called.
- The system will then call
getNextRequestedUpdateDateWithHandler
so you can specify how long until your complication requires a new update.
Apple's documentation is quite clear that you should not ask for updates too often, or conduct too much processing in the complication code or you will exhaust your time budget and your complication will stop updating. So, my question is: where and when do you do the update?
For context, my scenario is a URL with return data that changes up to two times per hour.
The most obvious place in which to put the URL fetch code is func requestedUpdateDidBegin()
Fetch the data, store it, and if there's no change, just return. If there was a change then extend or reload the timeline.
However, a URL fetch can be costly. Alternatives:
- Put the code on the phone app and send it over with a
WCSession
, but if the user closes that app then the updates will no longer happen.
- Use push updates, but this isn't a web app, so I have no place to send them from.
- Obviously I will update all the data when the user interacts with the watch app, but that now means it only gets updated when the user uses the app, which negates the need for a complication.
Is there anywhere else? Can I have a periodic function in the watch app that isn't part of the complication? Where is the right place to fetch the data for a complication update?
For watchOS 3, Apple recommends that you switch from using the complication datasource getNextRequestedUpdateDate
scheduled update to update your complication.
The old way for watchOS 2
requestedUpdateDidBegin()
is really only designed to update the complication. Keeping your complication (and watch app) up to date usually involves far more than reloading the timeline (and asynchronously retrieving data never fit in well with the old approach).
The new way for watchOS 3
The new and better approach is to use background refresh app tasks. You can use a series of background tasks to schedule and handle your app extension being woken in the background to:
Fetch new data
- using WKWatchConnectivityRefreshBackgroundTask to obtain data from the phone, or
- using WKURLSessionRefreshBackgroundTask to download data from a server
- update your model once the data arrives,
- update your complication from the model (by reloading or extending the timeline), and finally
- update your app's dock snapshot to show the data on the dock
Call each tasks’s setTaskCompleted
method as soon as the task is complete.
Other benefits of using app tasks
One of the key features about this design is that the watch extension can now handle a variety of foreground and background scenarios which cover:
- initially loading data when your app/complication starts,
- updating data in the background, when the extension is woken by a background task, and
- updating data in the foreground, when the user resumes your app from the dock.
Apple recommends that you use each opportunity you are given regardless of whether your app is in the foreground or background to keep your complication, app, and dock snapshot up to date.
Are there any limitations?
The number of total available tasks per day is divided among the number of apps in the dock. The fewer apps in the dock, the more tasks your app could utilize. The more apps in the dock, the fewer you can utilize.
If your complication is active, your app can be woken up at least four times an hour.
If your complication is not active, your app is guaranteed to be woken at least once an hour.
Since your app is now running in the background, you're expected to efficiently and quickly complete your background tasks.
Background tasks are limited by the amount of CPU time and CPU usage allowed them. If you exceed the CPU time (or use more than 10% of the CPU while in the background), the system will terminate your app (resulting in a crash).
For more information
A good introduction explaining when and why to update your watch app is covered in Designing Great Apple Watch Experiences.
For specifics, the Keeping Your Watch App Up to Date session covers everything you need to know to keep your complication, app, and dock snapshot up to date.
WatchBackgroundRefresh sample code demonstrates how to use WKRefreshBackgroundTask
to update WatchKit apps in the background.
Edit: El Tea (op) has posted a good answer at https://stackoverflow.com/a/32994055/630614
This is an interesting question/problem, and I've been wondering about a lot of the same!
For the most part, it seems that when I'm working on a new complication I need to step back and see when I really want to update it. A "countdown" complication could set all future timeline entries at one time, when the "end date" is set. An app that shows the current status of a web service could have relevant data stored in NSUserDefaults
when an APNS comes through.
If you don't have access to APNS, don't want to run your iOS app in a background mode, and don't want to make HTTP requests from Apple Watch, I can think of 2 other options.
1) Schedule local notifications. The good part is that your Apple Watch should run didReceiveLocalNotification
, but the bad part is that the user will get a notification when you're simply trying to check the status without a disruption.
2) Send a message to iOS via sendMessage(_:replyHandler:errorHandler:)
in your reloadTimelineForComplication
method, setting nil
for the replyHandler
to make it as quick as possible:
Calling this method from your WatchKit extension while it is active and running wakes up the corresponding iOS app in the background and makes it reachable.
Your iOS app could perform whatever network requests are needed and then store the information or push it to Apple Watch. Unfortunately, I don't think the watch extension will have it's session.didReceive...
called until you run it, but you could access the data on the next call to requestedUpdateDidBegin
.
As I said, I'm very interested in this same thing, so post some thoughts back and maybe we can extrapolate on some best practices here.