Background URL session upload task behavior in watchOS?

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 and indeed, I do see this reliably being triggered every 15-20 minutes or so.

In my handle(_:) method, I process the WKApplicationRefreshBackgroundTask 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 extension 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)
        }
    }
}

And here is how I'm creating and starting the background URL session upload task:

func startBackgroundURLSessionUploadTask() {

    // 1. check to see that we have locations to report; otherwise, just return
    // 2. serialize the locations into a temporary file
    // 3. create the background upload task

    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() // this is a POST request
    let backgroundUrlSessionUploadTask = backgroundUrlSession.uploadTask(with: request, fromFile: tempFileUrl)
    backgroundUrlSessionUploadTask.countOfBytesClientExpectsToSend = Int64(serializedData.count) // on average, this is ~1.5 KB
    backgroundUrlSessionUploadTask.countOfBytesClientExpectsToReceive = Int64(50) // approximate size of server response
    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:).

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 myhandle(_:) method. But when I start the background URL session upload task (in startBackgroundURLSessionUploadTask()), I was expecting that when the upload completes, I'd receive another call to myhandle(_:) method with an instance of WKURLSessionRefreshBackgroundTask 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!

Replies

Did you ever figure this out? I’d like to transfer much larger files (hundreds of MB total) using this approach.