Updating a WatchOS Complication from an API Call

Our free App, Can I Skate, has had a complication for a couple of years now. In fact I wrote the app partly to see how to develop complications and widgets. Keeping the complication updated with fresh status information from the web API has always been complicated and painful and flaky. Every time someone would complain that the watch complication wasn’t showing the right state I’d groan and mutter something about caching and tell them to just wait a bit, hoping they’d get bored and forget about it so I could also. A recent update made me look at the complication code again and try to make it update more reliably.

From ClockKit to WidgetKit

My previous version was using ClockKit which is now completely deprecated by Apple, it has been replaced by WidgetKit. The good news is that there’s an excellent article about migrating your ClockKit complication to WidgetKit from Apple. And actually doing it is not that hard. After adding the new target I copied a bit of code into the sample Widget and moved some assets over and it displayed some static information. Now all I had to do was to get it to update. 😢

Background Where Now?

The previous version of the code used the standard background URL session process. It’s a bit harder to setup in WatchOS for a complication because you have to use the WKApplicationDelegate to schedule your background URL session and hook it into the app launch. It sort of worked but TBH, it always felt like magic and I never liked it. I think you might be able to still get this working for a WidgetKit complication but if so I couldn’t figure it out. I got stuck trying to schedule the first download session. It wasn’t hooked into the app launch anymore because the new target created for WidgetKit didn’t seem to be connected the same way. Turns out, that was a great problem to have because it forced me to read at least thirty other pieces of documentation, articles, StackOverflow questions, etc. to try to figure out how to do it I finally came across the magic sentence in Apple’s docs that cast aside the clouds like a ray of sunshine.

The Magic Sentence

You can find the magic sentence in this page about the TimelineProvider along with a code sample. Here’s the quote:

If your provider needs to do asynchronous work to generate the timeline, such as fetching data from a server, store a reference to the completion handler and call it when you are done with your asynchronous work.

Not sure how many times I read this before it finally clicked for me and I figured out what seems like a really simple way to fetch data and update my complication. A brief digression into why I had so much trouble understanding this might help. Watch complications are driven by a timeline. The idea is that you provide a set of entries that should display on the watch at a particular time in the future. That works great if you know in advance what you’ll want t display for the next period of time, like every fifteen minutes for the next hour. Something like the phases of the moon as a complication would be a good example of this, you know in advance what they’ll be an when they should change so you could generate a timeline for a while into the future and the watch OS could handle the rest.

For many use cases of course this isn’t possible, you don’t know in advance what you want to display, you have to fetch new data to figure out what to display each time you do an update. Watch complications will only update themselves a limited number of times, you can’t just constantly keep them up to date so you can’t just keep polling the server, even if WatchOS allowed it it would use too much power. So you have to produce a timeline and update when the timeline updates. Turns out there is actually a very simple way to do that, and here it is:

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        fetchLiveStatus() { statusIndicator in
            let timeline = Timeline(entries: [StatusEntry(date: Date.now, status: statusIndicator)], policy: .atEnd)
            completion(timeline)
        }
    }

private func fetchLiveStatus(completion:@escaping (StatusIndicator) -> ()) {
      let url = URL(string: "https://www.githubstatus.com/api/v2/status.json" )
      var request = URLRequest(url: url)
      request.httpMethod = "GET"  // optional
      request.setValue("application/json", forHTTPHeaderField: "Content-Type")
      let task = URLSession.shared.dataTask(with: request){ data, response, error in
      if let error = error {
        completion( StatusIndicator.unknown)        
        return
      }

      guard let data = data else {
        completion( StatusIndicator.unknown)
        return
      }

      do {
        let status = try JSONDecoder().decode(GitHubStatus.self, from: data)
        completion( status.currentStatus
      } catch let jsonError {
        completion( StatusIndicator.unknown)
      }

    }

There’s a fair amount going on here but most of it is pretty standard Swift. The only part that interacts with the Complication is the getTimeline method which is defined by the Complication protocol. That method creates a Timeline where each entry in the timeline has to provide enough information for your Complication UI code to update itself. It provides that Timeline to the Complication by calling the completion method provided as a parameter to the getTimeline function.

In this implementation the data required for the timeline entry comes from another function, fetchLiveStatus, which provides a completion handler with a StatusIndicator as a parameter. StatusIndicator is an enum defined elsewhere that says whether the site is up, down or unknown. So getTimeline calls fetchLiveStatus and provides a completion handler. That completion handler creates a new TimeLine with a single entry containing the status that it received. It then calls the completion function provided as a parameter in the getTimeline function. That completion function is what provides the updated timeline to the Complication which can then update itself using the information provided. The complication decides based on the status what to show on the watch face, if the site is up it’s green, down it’s red, unknown it’s yellow, for example.

The fetchLiveStatus function is pretty simple, it retrieves some JSON from a URL which is an asynchronous operation. Once it completes the result is decoded and then fetchLiveStatus calls its completion handler with either the status it found in the JSON or StatusIndicator.unknown if something went wrong. In every case the completion handler is called, this is important to avoid blocking future updates to your Complication. Note that there is nothing about the implementation of the fetchLiveStatus that is special in any way to being part of a Watch Complication. It is just standard Swift code. All the requirements of the Complication are handled in the getTimeline call. I’ve included the code for fetchLiveStatus just to hopefully help make the full lifecycle a bit clearer.

One other important point about the Timeline returned is this bit: policy: .atEnd which tells the Complication that when it’s finished with all of the entries in this TimeLine (there’s only one) it should call getTimeline again. WatchOS decides how often to make that call based on a set of resource optimizations that don’t matter much when you’re creating your complication. The short version is that you can’t count on real time updates, it will only do it some number of times a day, and you need to make sure it’s frequent enough to meet the needs of your complication. If it’s not frequent enough there isn’t much you can do other than to change your goals so it is.

If you need an alternative you can look at pushing messages from your iOS App directly to the watch Complication, there is a facility for inter-device communication that can be used to update the complication. I haven’t needed that so you’re on your own.