I’m working on an independent watchOS app which is primarily designed to to collect and periodically send location updates to a server. The UI features a toggle that allows the user to turn this capability on or off at their discretion. The typical use case scenario would be for the user to turn the toggle on in the morning, put the app in the background and then go about their day.
Given the limitations and restrictions regarding background execution on watchOS, in an ideal situation, I would be able to upload the stored location updates about every 15-20 minutes. With an active complication on the watch face, it’s my understanding that this should be possible. I’ve implemented background app refresh (BAR) and indeed, I do see this reliably being triggered every 15-20 minutes or so.
When BAR occurs, it results in a callback to my handle(_:)
method which is defined in my WatchKit application delegate like this:
func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
backgroundTasks.forEach { task in
switch task {
case let appRefreshBackgroundTask as WKApplicationRefreshBackgroundTask:
// start background URL session to upload data; watchOS will perform the
// request in a separate process so that it will continue to run even if
// our app gets terminated; when the system is done transferring data, it
// will call this method again and backgroundTasks will contain an instance
// of WKURLSessionRefreshBackgroundTask which will be processed below
startBackgroundURLSessionUploadTask()
scheduleNextBackgroundAppRefresh()
appRefreshBackgroundTask.setTaskCompletedWithSnapshot(false)
case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
// add urlSessionTask to the pendingURLSessionRefreshBackgroundTasks array
// so we keep a reference to it; when the system completes the upload and
// informs us via a URL session delegate method callback, then we will
// retrieve urlSessionTask from the pendingURLSessionRefreshBackgroundTasks
// array and call .setTaskCompletedWithSnapshot(_:) on it
pendingURLSessionRefreshBackgroundTasks.append(urlSessionTask)
// create another background URL session using the background task’s
// sessionIdentifier and specify our application delegate as the session’s
// delegate; using the same identifier to create a second URL session allows
// the system to connect the session to the upload that it performed for us
// in another process
let configuration = URLSessionConfiguration.background(withIdentifier: urlSessionTask.sessionIdentifier)
let _ = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
default:
task.setTaskCompletedWithSnapshot(false)
}
}
}
So as you can see, when I handle the background app refresh task, I'm creating and resuming a new background URL session upload task. Here's what that looks like:
func startBackgroundURLSessionUploadTask() {
// 1. check to see that we have locations to report; otherwise, return
// 2. serialize the locations into a temporary file
let configuration = URLSessionConfiguration.background(withIdentifier: Constants.backgroundUploadIdentifier)
configuration.isDiscretionary = false
configuration.sessionSendsLaunchEvents = true
let backgroundUrlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let request: URLRequest = createURLRequest()
let backgroundUrlSessionUploadTask = backgroundUrlSession.uploadTask(with: request, fromFile: tempFileUrl)
// on average, we'll send ~1.5KB - 2.0KB of data
backgroundUrlSessionUploadTask.countOfBytesClientExpectsToSend = Int64(serializedData.count)
// the server response is ~50 bytes
backgroundUrlSessionUploadTask.countOfBytesClientExpectsToReceive = Int64(50)
backgroundUrlSessionUploadTask.resume()
}
Note that I'm not setting the .earliestBeginDate
property on the backgroundUrlSessionUploadTask
because I'd like the upload to start as soon as possible without any delay. Also, this same class (my WatchKit application delegate) conforms to URLSessionTaskDelegate
and I have implemented urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:)
and urlSession(_:task:didCompleteWithError:)
. When the upload happens, I see these two delegate methods being called. At the end of urlSession(_:task:didCompleteWithError:)
, I call .setTaskCompletedWithSnapshot(false)
on the WKURLSessionRefreshBackgroundTask
instance that I stored in the pendingURLSessionRefreshBackgroundTasks
array.
In my testing (on an actual Apple Watch Ultra running watchOS 9.3.1), I've observed that when the system performs the background app refresh, I always receive a callback to my handle(_:)
method. But when I start the background URL session upload task (in startBackgroundURLSessionUploadTask()
), I was expecting to receive another call to the myhandle(_:)
method (with an instance of WKURLSessionRefreshBackgroundTask
) when the upload has been completed but this doesn't seem to happen consistently. Sometimes I do see it but other times, I don't and when I don't, the data doesn't seem to be getting uploaded.
On a side note, most of the time, startBackgroundURLSessionUploadTask()
gets called as a result of my code handling a background app refresh task. But when the user turns off the toggle in the UI and I stop the location updates, I need to report any stored locations at that time and so I call startBackgroundURLSessionUploadTask()
to do that. In that specific case, the upload seems to work 100% of the time but I definitely don't see a callback to my handle(_:)
method when this occurs.
Am I wrong in expecting that I should always be getting a callback to handle(_:)
when a background URL session upload task completes? If so, under what circumstances should this occur? Thanks very much!