Where and When to get data for Watch Complication
Asked Answered
W

2

33

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?

Westfahl answered 27/9, 2015 at 23:34 Comment(4)
I've also noted that it may not be the case that requestedUpdateDidBegin() counts against your time budget. Calls to both the reload and extend both clearly do. So that has me leaning towards doing it in requestedUpdateDidBegin() in the absence of other info, but I am still in search of an actual answer rather than my best guess....Westfahl
Would you mind sharing some code on how to implement requestedUpdateDidBegin, reloadTimeLineForComplication, and getCurrentTimelineEntryForComplication? I've been trying to implement them to perform a network request and update the complication every two hours, but absolutely nothing happens after the initial setting of the complication. It always displays the original data.Eloiseelon
B K - I'd be happy to. Why not ask a new question and link it? I'll put my code in the next day.Westfahl
I actually asked it last night! Here's the link: https://mcmap.net/q/453526/-what-is-the-flow-for-updating-complication-data-for-apple-watch/4959716Eloiseelon
G
29

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:

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

Gab answered 16/6, 2016 at 18:44 Comment(2)
Is there a preferred way to update location in the background, say if you needed the current location before doing a URLSession?Jori
WARNING! With watchOS 4 it is obligatory to use the newer method. Apps that were built using the old method will have their complications simply fail to update at all if compiled under iOS11/watchOS4 and then getNextRequestedUpdateDate does nothing apart from giving a warning message in the console when it is called.Johnettajohnette
T
3

Edit: El Tea (op) has posted a good answer at https://mcmap.net/q/453526/-what-is-the-flow-for-updating-complication-data-for-apple-watch

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.

Takeoff answered 28/9, 2015 at 16:40 Comment(19)
Agree: First know why you need the update. In my case, the data expires and is only good for one hour, so my timeline can have a bunch of history, but changes every hour. So this necessitates a check on the internet every hour. I may not know enough about running the app in background mode though. It sounds like option #2 you have presented is a good option (WCSession). The reply after that is easy: there is a new message in OS 2: transferCurrentComplicationUserInfo: that counts against the time budget.Westfahl
So you could wake the app up with a sendMessage in response to requestedUpdateDidBegin() have the phone check for new data, and send it back if there's new data for the timeline. However, at that point you might as well have the phone do all the checking on a periodic rate, yes? Here's where my knowledge is lacking though: if the app is closed on the phone, this will never happen. So maybe the pair of the WCSession communication is the best way...Westfahl
But the other question is: can you run an HTTP request from the watch in a way that doesn't count against the time budget?Westfahl
Right, when you close the iOS app it can request additional time, but from what I understand that's a maximum of 10 minutes to perform whatever tasks are needed. The only other option for continuing the running is in a "background mode", i.e. voip call, map directions, etc., where the app is still running with the green bar up top. That's definitely not what we're looking for.Takeoff
transferCurrentComplicationUserInfo sounds perfect! Please try and post back here with results :)Takeoff
I'm getting closer. The documentation for sendMessage states that "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." Do you know from what state it can wake it up? What if the user never uses the app?Westfahl
Visit developer.apple.com/library/ios/documentation/iPhone/Conceptual/… and scroll down to table 3.1. Perhaps you actually should be running in a background mode of fetch. I'd like to make a small sample app that does exactly this + communicates the data back to a complication.Takeoff
That's actually my current plan, well part of it anyway. From what I've read, with phone running in fetch you still can't guarantee you'll get the update requests. (Either you don't get scheduled or your app was force quit by the user). So, I'll leave the watch on its own timer, but when it starts to update it will check to see if the phone previously provided data. If it hasn't, it will ask the phone for an update via WCSession. If it can't reach the phone, then it will initiate its own URL grab.Westfahl
@ElTea Did you end up writing a small sample app to see if you could find a solution for this? Did you find anything useful?Photophobia
@Philippe: Sorry; this is an evening hobby. I have some stuff and will update tomorrow. It's complicated as it really depends on your use case, but I'll try and go through a few scenarios.Westfahl
Here's the problem: we're looking for best practices but we don't know where Apple is tracking execution usage. In the documentation, requestedUpdateDidBegin() doesn't specifically mention execution budget. However, reloadTimelineForComplication() states: "Call this method sparingly" and that "If your complication has already exceeded its allotted daily budget for execution time, calls to this method do nothing." My plan now is to write a program that shows if requestedUpdateDidBegin() is consuming execution budget. If I can't prove that, then I still have some best practices to propose.Westfahl
So far have pushed both functions to 10 minute-interval updates and consuming CPU for 13s of execution time over an hour and I can't run out of time budget. It may not work in the sim. I just doubled usage in reload...() and if I can't push it to running out of budget then I will have to try running on the actual hardware.Westfahl
Ran a longer test on the hardware in debug. Asked for 5-minute updates which resulted in updates about every 10 minutes. Each update resulted in getCurrentTimelineEntryForComplication() being called twice (I had two instances of the complication on the watch face) and each time the code calculated prime numbers to 20,000, consuming 2.5s (as measured between start/stop NSDate() references). After 15 update cycles, they stopped for 3h45m but without requestedUpdateBudgetExhausted() being called. Any ideas on why it wasn't called?Westfahl
Interesting. So it sounds like the time budget may not be as strict as Apple made it sound. I'm going to try and write an app that pulls from the web once every ten minutes and see if that eventually runs out of budget.Photophobia
I'm tending to agree. New app is running in release and is executing at the same rate (every 10 minutes), but only one calculation every refresh. It's at 45 refreshes now, consuming 38s of CPU. I'm thinking as long as you make sure you get data using an asynchronous NSURLSession and try to keep it two twice an hour or less you're probably going to be fine. Which means that most of the rest of the best practices I have in mind aren't really needed.Westfahl
Final results from previous test: 60 calculations done in 10-min increments, over roughly 10 hours. 52s of CPU-ish time taken. NEVER saw an unreasonable delay and had 0 requestedUpdateBudgetExhausted() calls. So I really don't know how Apple is limiting this. What I do know is that it killed my battery. My normal usage gets me to bedtime at 50% and my watch just died. So clearly you don't want to put a bunch of computation on the watch. I will look at lessons learned and best practices tomorrow, time permitting.Westfahl
I finally received a call to requestedUpdateBudgetExhausted() after roughly 100 updates at 10 minute intervals. The updates via requestedUpdateDidBegin then resumed again about 3 hours later.Johnettajohnette
That's awesome. Thank you for sharing @Johnettajohnette !Takeoff
I'm also finding that if I supply a 15min interval to getNextRequestedUpdateDateWithHandler(), then the next requestedUpdateDidBegin() typically occurs on the next 10min clock boundary AFTER the requested interval expires e.g. if at 10:10am I set a 15min interval, the next update event actually occurs at 10:20am. So there seems to be this 10m granularity in update intervals AND which are synced to clock time boundaries. If correct, this is not so good for my server, as it means devices will be sending out update requests in batches all synced to those 10 minute clock boundaries.Johnettajohnette

© 2022 - 2024 — McMap. All rights reserved.