9

I'm working on a complication to get scheduled data from a web service. Every 20-30 minutes (or manually), I am scheduling a WKRefreshBackgroundTask to do this.

As suggested by Apple, I want the OS to handle the fetching of this data via a background NSURLSession.

This is the function I use to download the data I need:

func scheduleURLSession()
{
    print("\nScheduling URL Session...")

    let backgroundSessionConfig:URLSessionConfiguration = URLSessionConfiguration.background(withIdentifier: NSUUID().uuidString)
    backgroundSessionConfig.sessionSendsLaunchEvents = true

    let backgroundSession = URLSession(configuration: backgroundSessionConfig)

    let downloadTask = backgroundSession.downloadTask(with: URL(string: "https://www.myserver.com/somedata")!)
    downloadTask.resume()
}

A few things about this:

  1. It does get called when I schedule it. I see its print statement in the console.
  2. It's nearly identical to Apple's example.
  3. I've omitted the URL. That same URL works in the iOS/watchOS apps just fine so nothing is wrong with it.

The problem is, even though I am calling resume() on the task and allowing it to wake my app when it is complete, it doesn't seem to do either.

When it completes, it should be coming back to a WKExtensionDelegate handler:

func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>)

as a WKURLSessionRefreshBackgroundTask, but it does not.

My code, identical to Apple's sample code, then creates another session but rejoins it via the WKURLSessionRefreshBackgroundTask's identifier. This is where the delegate is set in order to process the downloaded data. Check the code:

func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
    for task in backgroundTasks {
        switch task {

        case let backgroundTask as WKApplicationRefreshBackgroundTask:
            print("\nWatchKit - WKApplicationRefreshBackgroundTask")
            // self.updateComplicationDataArrivalTimes(backgroundTask)
            self.scheduleURLSession()
            backgroundTask.setTaskCompleted()

        case let snapshotTask as WKSnapshotRefreshBackgroundTask:
            // Snapshot tasks have a unique completion call, make sure to set your expiration date
            print("\nWatchKit - WKSnapshotRefreshBackgroundTask")
            snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)

        case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
            print("\nWatchKit - WKWatchConnectivityRefreshBackgroundTask")
            connectivityTask.setTaskCompleted()

        case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
            print("\nWatchKit - WKURLSessionRefreshBackgroundTask")
            let backgroundConfigObject = URLSessionConfiguration.background(withIdentifier: urlSessionTask.sessionIdentifier)
            let backgroundSession = URLSession(configuration: backgroundConfigObject, delegate: self, delegateQueue: nil)

            print("Rejoining session ", backgroundSession)
            urlSessionTask.setTaskCompleted()

        default:
            // make sure to complete unhandled task types
            task.setTaskCompleted()
        }
    }
}

But again, it doesn't seem to ever come back. I can't seem to figure out why this is working despite the code is identical to Apple's sample code for this project: WatchBackgroundRefresh: Using WKRefreshBackgroundTask to update WatchKit apps in the background.

Is there some setting in my project that I am missing? I am putting all this code in the ExtensionDelegate, new in watchOS 3. I am also conforming to WKExtensionDelegate and URLSessionDownloadDelegate.

Thanks for your help in advance!

Mario A Guzman
  • 3,483
  • 4
  • 27
  • 36
  • Did you ever solve this? – Jens Peter Jul 09 '17 at 08:04
  • 1
    It's very cumbersome. When I submitted a bug, the Apple rep told me that this only works on a physical device (not the simulator). I wish they had said this in the actual documentation for the sample project they posted on their site. Also, it takes a while. You have to actually wait for the callback for however long you schedule it. Also, it seems as if you could run out of your daily allotted background work tasks because after a while, it seems to stop working till the next day - when the system allows for more background work tasks. – Mario A Guzman Jul 09 '17 at 17:48
  • Thanks! Thats.... well, terrible news. But it also explains why I can't get Apples example working in the simulator. I don't own a Apple Watch, so either I buy one or retract my app :( – Jens Peter Jul 10 '17 at 13:09
  • Yeah, it sucks. I really hope Apple fixes it for the simulator at least. Have you tried with iOS 4 and Xcode 9 beta? – Mario A Guzman Jul 10 '17 at 13:53
  • @MarioAGuzman Do you happen to have by any chance a working snippet of this? I am struggling to get this working (also similar, reading data from API in some time interval) but can't put it all together, hope you can help. – ZassX Jan 31 '18 at 10:24

2 Answers2

6

I got it working! I started out by looking a lot at the WWDC'16 videoes/notes, comparing them to the WatchBackgroundRefresh example from Apple. Then, while browsing the Apple Dev forums, I found this thread, with the same problem we encountered. There, a guy, "Daniel Fontes" posts his 'recipe' for getting it to work (note, not the accepted answer!), and that combined with the videos worked for me.

What I did to make the Apple example work:

  1. Make the MainInterfaceController an URLSessionDelegate
  2. Create a local variable: var savedTask:WKRefreshBackgroundTask?
  3. In the handle( backgroundTasks:) function, save the WKURLSessionRefreshBackgroundTaskto the local variable self.savedTask = task - do not task.setTaskCompleted() (!!)
  4. In the urlSession - didFinishDownloading:, set the saved task to completed: self.savedTask?.setTaskCompleted() after you've read the data received.
  5. Ensure that the resource you are accessing is https://, otherwise add the "App Transport Security Settings" to your Info.plist file.

Hope this can help!

ps. this works in the simnulator (xcode 8.3.3, watchOS 3.2)

Jens Peter
  • 715
  • 6
  • 10
  • Great tip @Jens Peter!! IT finally woked! My mistake was that I was using another class for the delegate. But now the delegates are on my ExtensionDelegate and it works. But two things I can’t solve: 1 - I keep getting this message that the background session with the identifier I use already exists. It bothers me, but things work anyhow. 2 - where do you put the `invalidateAndCancel()` call? I am not using it. Should I? – francisaugusto Feb 03 '18 at 07:48
4

I never did get Apple's example working, but I did get background refresh working. Unlike their example you will need to make yourself the delegate of the session. So create your background session like this instead:

let backgroundSession = URLSession(configuration: backgroundSessionConfig, delegate: self, delegateQueue: nil)

I have a detailed code example here (see the question's code):

WatchOS 3 WKApplicationRefreshBackgroundTask didReceiveChallenge

Hope it helps.

Community
  • 1
  • 1
CodenameDuchess
  • 1,221
  • 18
  • 24