WatchOS: background refresh file is downloaded but didFinishDownloadingTo: never called...

Greetings,
I've implemented background refresh in my WatchOS app, its purpose is to get some data (always less than 400 bytes) from the server at a minimum of every 15 minutes and update the complication.

Sounds pretty straight forward and it's like clockwork - works really well when I'm debugging the WatchOS app in Xcode or started by launching it manually.

The problem begins if I do something to force the system to kill the app by simply swiping through all the watch faces, or loading other apps. This is a highly realistic scenario.

At this point I switch to observing what is happening on the watch by viewing the console output:
  1. WatchOS launches my app in the background and my extension's handle() method is called

  2. This in turn invokes my code which creates a properly crafted URLRequest which is then passed to a background URLSession's download task.

  3. The very small (<400 bytes) file is downloaded. This works every time, regardless of how the app is launched.

  4. My URLSessionDownloadDelegate's urlSession(_ downloadTask:didFinishDownloadingTo:) method is NOT called, even though the file has been downloaded successfully!

  5. Even stranger, the delegate method IS then called only if I then tap on my complication which launches the app.


Please tell me that this is not how the intended functionality is supposed to work. How am I supposed to update the complication in the background if the only time I can process the downloaded data is if the user starts the app?

I was under the (hopefully not mistaken) impression that if I have my app's complication on the Watch face I would be able to process the result and update the complication data in the background. Is that a wrong impression?

If I am mistaken, then the complication cannot be updated 4 times per hour, and what is the point then of downloading the data in the background if the user then has to tap on the stale complication?

Thanks in advance,
Sal


Answered by sal_from_new_york in 636388022
After much testing on both WatchOS 6 & 7 here's what I've determined:
  1. WatchOS 7 appears to have a better scheduling algorithm than WatchOS 6. Can't prove it though.

  2. If my app has died and WatchOS starts it, although the scheduling of the background refresh still happens, the URLSessionDownloadDelegate's urlSession(_ downloadTask:didFinishDownloadingTo:) method is NOT called until the user either taps on one of my complications or starts the app from the home screen.

  3. The times which I've observed  skips in refresh, and my complication now says it's been over 15 minutes long is because a network error such as a timeout has occurred.

  4. It appears that keeping a reference to the latest URLSession is worth doing, just ensure that you remove the reference when it has completed successfully or otherwise.

  5. Using earliestBeginDate had a detrimental effect on my app. It would keep getting terminated by the system as soon as I resumed the first URLSession.

Although it is stated in the documentation that the OS will retry the request if a network error has occurred, I can't determine if that's true since I don't know how many times it actually retried.

Specifically, when do you schedule the next URLSession task after the previous one is completed?

Apple has previously posted guidance saying that URLSession penalizes apps for repeated launching and requires at least 10 minutes between tasks. Does that mean you have to wait 10+ minutes before you can even *schedule* the next URLSession? I started waiting until the next background app refresh to schedule the next URLSession task and saw some improvement, but that also introduces a big delay since sometimes the next background app refresh doesn't trigger for 15+ minutes.

The sample code shown in one of this year's WWDC videos had the next URLSession getting scheduled right when the previous one completed, but other Apple docs and my testing seems to indicate that's not correct.
@developer555,
The NSURLSession task is scheduled when the system activates my app in the background and calls the handle(_:) method in my ExtensionDelegate. When the task type is WKApplicationRefreshBackgroundTask I take 2 actions:
  1. Schedule the next background refresh to occur in 15 minutes

  2. Call a method which then creates a background URLSession configuration, and then creates a URLSession for my network request. After creating the session, I resume it just before leaving that method.

Code Block
func handle(_ bkTasks: Set<WKRefreshBackgroundTask>) {
for aTask in bkTasks {
switch aTask {
case let bkTask as WKApplicationRefreshBackgroundTask:
scheduleNextAppBackgroundRefresh()
createAndResumeURLSession()
bkTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
/* This adds the urlSessionTask to a list, which will be used to mark
all tasks as completed upon successful download of data
*/
handleDownload(urlSessionTask)
default:
// Ensure that unhandled tasks are marked as complete
aTask.setTaskCompletedWithSnapshot(false)
}
}




At some point in the future I may get back data from the NSURLSession. Note that the neither the background refresh nor the URLSession task scheduling is contingent upon getting data successfully. This is because there may be network issues such as timeouts which occur. Please note that this is all boilerplate code, and you should be able to find some examples on the net

Hope this helps. Let me know

Note: This has me thinking as to whether I need to take any actions such as mark tasks completed if an error occurs during the download. I suspect I need to.



Do you use earliestBeginDate on the NSURLSession download task to schedule it for a future date?

And if you end up scheduling multiple download tasks because one of the previous ones didn't complete, do you ever see multiple tasks completing at the same time at some point in the future? How do you handle those?
Accepted Answer
After much testing on both WatchOS 6 & 7 here's what I've determined:
  1. WatchOS 7 appears to have a better scheduling algorithm than WatchOS 6. Can't prove it though.

  2. If my app has died and WatchOS starts it, although the scheduling of the background refresh still happens, the URLSessionDownloadDelegate's urlSession(_ downloadTask:didFinishDownloadingTo:) method is NOT called until the user either taps on one of my complications or starts the app from the home screen.

  3. The times which I've observed  skips in refresh, and my complication now says it's been over 15 minutes long is because a network error such as a timeout has occurred.

  4. It appears that keeping a reference to the latest URLSession is worth doing, just ensure that you remove the reference when it has completed successfully or otherwise.

  5. Using earliestBeginDate had a detrimental effect on my app. It would keep getting terminated by the system as soon as I resumed the first URLSession.

Although it is stated in the documentation that the OS will retry the request if a network error has occurred, I can't determine if that's true since I don't know how many times it actually retried.

WatchOS: background refresh file is downloaded but didFinishDownloadingTo: never called...
 
 
Q