reloadTimeline() doesn't update complication

2.1k Views Asked by At

I'm trying to make a watchOS 3 app, and I want to update my complication in a background task.

First, I get new data from a server in a background task within handle(). After that, I update my active complications by calling complicationServer.reloadTimeline(for:).

In the console I do see the message "UPDATE COMPLICATION," so the code is executed.

Yet after reloading, the complication still shows the old data. If I switch the watch face and switch back, then the complication sometimes reloads. Do I have to do something else to reload the complication from the background task?

func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
    for task : WKRefreshBackgroundTask in backgroundTasks {
        if (WKExtension.shared().applicationState == .background) {
            if task is WKApplicationRefreshBackgroundTask {
                let dataProvider = DataProvider()
                dataProvider.getData(station: "Name", completion: { (data, error) in
                    self.updateComplication()
                    self.scheduleNextBackgroundRefresh()

                    task.setTaskCompleted()
                })
            }
        } else {
            task.setTaskCompleted()
        }
    }
}

func updateComplication() {
    let complicationServer = CLKComplicationServer.sharedInstance()

    for complication in complicationServer.activeComplications! {
        print("UPDATE COMPLICATION")
        complicationServer.reloadTimeline(for: complication)
    }
}
1

There are 1 best solutions below

1
On BEST ANSWER

Your current approach:

You've got a mix of watchOS 2 and watchOS 3 approaches there.

  • WKApplicationRefreshBackgroundTask (new watchOS 3 approach)
    1. DataProvider starts asynchronous transfer (old watchOS 2 approach which assumes it's in the foreground, not the background).
      • That background thread (of a background task which may or may not be suspended yet not completed) expects to have the background task handle its completion, once the asynchronous transfer completes.

In short, you've expected your background refresh task to wait in the background for an asynchronous transfer. This is bit convoluted (since the refresh task is supposed to be doing work in the background, not waiting on other work to complete).

A better way for watchOS 3:

Since an asynchronous transfer can be suspended, you would be better off using a URLSession background transfer.

Always upload and download data using an URLSession background transfer. Background transfers occur in a separate process. They continue to transfer the data even after your app has terminated. Asynchronous uploads and downloads, on the other hand, are suspended with your app. Given the short run time of watchOS apps, you cannot guarantee that an asynchronous transfer will finish before the app is suspended.

By letting a WKURLSessionRefreshBackgroundTask respond to the background transfer, your extension can be woken in the background once the session completes, hand off that session's data to the data provider, and then update the complication.

An suggestion about the data provider:

It seems to have the responsibility of both transferring data, and providing data. You may want to consider splitting off the network portion into a separate component, and simply let it be a repository of data.

I get the impression that it's meant to be some manner of singleton (behind the scenes) yet you initialize an instance as a DataProvider().

From a readability viewpoint, it's not apparent from the provided code that the complication data source will be using the same data provider as the one who received the data.

You should avoid force unwrapping optionals:

When activeComplications is nil (such as when the complication had been removed from the watch face between the last update and this update), your code will ungraciously crash.

You should use a guard or if let to first check that you still have an active complication.