What is the flow for updating complication data for Apple Watch?
Asked Answered
B

2

22

I've been following a lot of tutorials on the internet to learn how to set up the complication. I have no problem getting the complication set up as expected.

Until the initial timeline entries expire. After 12 hours, I do not know how to update it to keep the complication live. I'll share everything I have below and hopefully somebody can help fill me in.

Here, I create the variables for my data that I want to display on the complication.

struct data = {
var name: String
var startString: String
var startDate: NSDate
}

The following array is a container for this data.

var dataArray = [data]

This allows the complication to be shown when the watch is locked.

func getPrivacyBehaviorForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
    handler(.ShowOnLockScreen)
}

This allows forward Time Travel on the complication.

func getSupportedTimeTravelDirectionsForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
    handler([.Forward])
}

Here, I set the starting time of the timeline to be equal to now.

func getTimelineStartDateForComplication(complication: CLKComplication, withHandler handler: (NSDate?) -> Void) {
    handler(NSDate())
}

Here, I set the ending time of the timeline to be equal to 12 hours from now.

func getTimelineEndDateForComplication(complication: CLKComplication, withHandler handler: (NSDate?) -> Void) {
    handler(NSDate(timeIntervalSinceNow: (60 * 60 * 12)))
}

Here, I create the template of the complication. This is to show sample data for users when they see my complication while browsing all complications on their watch.

func getPlaceholderTemplateForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void) {

    let headerTextProvider = CLKSimpleTextProvider(text: "Some Data")
    let body1TextProvider = CLKSimpleTextProvider(text: "Some Data Time")
    let template = CLKComplicationTemplateModularLargeStandardBody()
    template.headerTextProvider = headerTextProvider
    template.body1TextProvider = body1TextProvider

    handler(template)
}

This creates the very first timeline entry for the complication. As soon as the complication is enabled, this code will be run and immediately populate the complication accordingly.

func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimelineEntry?) -> Void) {

    createData()

    if complication.family == .ModularLarge {

        if dataArray.count != 0 {

            let firstData = dataArray[0]
            let headerTextProvider = CLKSimpleTextProvider(text: firstData.name)
            let body1TextProvider = CLKSimpleTextProvider(text: firstData.startString)
            let template = CLKComplicationTemplateModularLargeStandardBody()
            template.headerTextProvider = headerTextProvider
            template.body1TextProvider = body1TextProvider
            let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: template)
            handler(timelineEntry)
        } else {
            let headerTextProvider = CLKSimpleTextProvider(text: "No Data")
            let body1TextProvider = CLKSimpleTextProvider(text: "Create some data")
            let template = CLKComplicationTemplateModularLargeStandardBody()
            template.headerTextProvider = headerTextProvider
            template.body1TextProvider = body1TextProvider

            let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: template)
            handler(timelineEntry)
        }

    } else {
        handler(nil)
    }

}

This is where I create timeline entries for all of the data that I currently have.

func getTimelineEntriesForComplication(complication: CLKComplication, afterDate date: NSDate, limit: Int, withHandler handler: ([CLKComplicationTimelineEntry]?) -> Void) {

    createData()

    var entries = [CLKComplicationTimelineEntry]()

    for dataObject in dataArray {

        if entries.count < limit && data.startDate.timeIntervalSinceDate(date) > 0 {

            let headerTextProvider = CLKSimpleTextProvider(text: dataObject.name)
            let body1TextProvider = CLKSimpleTextProvider(text: dataObject.startString)
            let template = CLKComplicationTemplateModularLargeStandardBody()
            template.headerTextProvider = headerTextProvider
            template.body1TextProvider = body1TextProvider
            let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(timeInterval: (-10*60), sinceDate: data.startDate), complicationTemplate: template)
            entries.append(timelineEntry)

        }

    }

    handler(entries)

}

This tells the watch when to update the complication data.

func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
    handler(NSDate(timeIntervalSinceNow: 60 * 60 * 6))
}

This is where I'm running into problems.

How do I create my new data and reload the timeline? What is the flow? I'm not trying to extend the timeline, but rather to completely replace it. I'm at a complete loss. Apple's docs are pretty vague when it comes to this point. I know that I need to implement the following methods, but I don't know how. Can somebody help me to fill in this code?

func requestedUpdateDidBegin() {
    createData() //I assume createData() goes here? If so, how do I populate the new timeline entries based on the results?
}

func requestedUpdateBudgetExhausted() {
    //This can't possibly be the case as I haven't gotten it to work once.
}

func reloadTimelineForComplication(complication: CLKComplication!) {
      //This method appears to do nothing.
}

Update:

Thanks to El Tea, I've got it working. I need to add an instance of CLKComplicationServer to requestedUpdateDidBegin and put the reloadTimeline method inside.

Here is the updated code:

func requestedUpdateDidBegin() {
    print("Complication update is starting")

    createData()

    let server=CLKComplicationServer.sharedInstance()

    for comp in (server.activeComplications) {
        server.reloadTimelineForComplication(comp)
        print("Timeline has been reloaded!")
    }

}

func requestedUpdateBudgetExhausted() {
    print("Budget exhausted")
}
Beers answered 6/10, 2015 at 14:50 Comment(1)
Ever run into a problem on the ComplicationController side where you can't seem to get the data you set in the ExtensionDelegate via WCSession: to be pulled? Basically in whatever code you have in your createData() function, if that is being pulled from the ExtensionDelegate via let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate yet not actually pulling any of that data. I'm stuck on it here: #35543229Auriscope
H
27

The flow for a complication refresh that is being done on a time interval follows this sequence:

  • iOS calls your function requestedUpdateDidBegin() or requestedUpdateBudgetExhausted() (If your budget is exhausted nothing you do will cause an update until you're given more execution time.)
  • Inside of requestedUpdateDidBegin() you have to call reloadTimelineForComplication() or extendTimelineForComplication() to specify which of your complications you want reloaded or to have data added to. If you don't do this, nothing happens!
  • Depending on if you called reload or extend, iOS makes calls to one or both of getCurrentTimelineEntryForComplication() and getTimelineEntriesForComplication()
  • Irrespective of whether or not you updated your complication, iOS calls getNextRequestedUpdateDateWithHandler() to find out when you next want the above steps to repeat.

Note: the last two steps don't necessarily have to happen in that order.

The process works this way so that iOS doesn't ask you to repeatedly regenerate the same data. It gives you a chance in requestedUpdateDidBegin() to decide if your complication needs updating. If it doesn't, your code should just return. (This reduces your complication's execution time and helps avoid iOS from cutting off your app from further updates for having used its daily budget). But if you do have new data, you need to tell iOS by calling reloadTimelineForComplication() or extendTimelineForComplication()

From what I can tell, everything you've written there looks good other than you weren't requesting a reload or extend inside requestedUpdateDidBegin(). It's possible for your complication to be visible on the watch face in more than one position, and for different templates to have different display behaviours, so you have to invalidate all of them. Here is what my code looks like:

func requestedUpdateDidBegin() {

    //Not shown: decide if you actually need to update your complication.
    //If you do, execute the following code:

    let server=CLKComplicationServer.sharedInstance()

    for comp in (server.activeComplications) {
        server.reloadTimelineForComplication(comp)
    }
}

Note that besides time intervals that there are other ways to initiate refreshes including push alerts, executing reloads when your watch app runs, or using the Watch Connectivity framework with a WCSession to have your phone app send update data to be displayed immediately via transferCurrentComplicationUserInfo(). See Updating Your Complication Data in Apple's docs for more info.

I've had success in the simulator testing update intervals as short as ten minutes. You probably shouldn't update that frequently on the real watch due to the execution time budget, but this will allow you to test your code without waiting 12 hours.

Hallowmas answered 7/10, 2015 at 13:51 Comment(6)
I'm trying it out! It must be that I didn't include the CLKComplicationServer. I'll let you know if it ends up working.Beers
Great! Complications are neat, but the info on them is still sorely lacking at this point. Glad you got it running.Hallowmas
This all works great if your createData() function is synchronous, but what about if it needs to fetch data from the iOS app? Is the best approach there to have the iOS app call transferCurrentComplicationUserInfo? A hacky, alternative approach would be to have requestedUpdateDidBegin call reloadTimelineForComplication on a delayed timer.Hannibal
Untested, but my approach would depend on how the data on the iOS app is generated. If the data on the phone is generated on its own, at its own pace, I think I would just use transferCurrentComplicationUserInfo() like you said to send from the phone to the watch when required. However, if the watch remains in control and is pulling the info, use a WCSession with sendMessage:replyHandler:errorHandler: to ask the phone for info. Either use the replyhandler to call reloadTimelineForComplication or don't use a handler and go back to transferCurrentCOmplicationUserInfo(). Good luck!Hallowmas
Please note that when requestedUpdateBudgetExhausted is called you are allowed to do one more update in that call. It is like a marker like one more time and then you will have to wait for x amount until the watch decides to replenish your budgetCanvas
How is it possible to launch iOS app from the watch 3 app?Hatchet
E
17

El Tea's answer goes into detail on how to update a complication for watchOS 2.

In watchOS 3, the recommended way to keep your complication up to date involves using background refresh app tasks. You can use a series of background tasks to schedule and handle your app extension being woken in the background to:

  • Fetch new data

  • update your model once the data arrives,
  • update your complication (by reloading or extending the timeline) to display the new data available from the model, and finally
  • update your app's dock snapshot to show the data on the dock

This is much more functional as well as energy efficient, as it does not use any of your complication's daily execution budget to fetch data or update the model.

It also avoids any complexity from unadvised approaches which tried to asynchronously fetch data in a complication datasource that was only ever meant to immediately respond to requests.

I've provided further information, as well as links to WWDC videos and sample code, in a different answer.

To sum up the change for watchOS 3

Use scheduled background tasks instead of getNextRequestedUpdateDateWithHandler().

Reload or extend your timeline in an app task, instead of within requestedUpdateDidBegin().

Eldridge answered 16/6, 2016 at 19:14 Comment(4)
this is a great write-up, but watchOS 3 is not available for public.Homoeroticism
Be aware that using WKWatchConnectivityRefreshBackgroundTask seems currently not to be possible, see here. It does not even work in Apple's demo project QuickSwitch.Cheadle
Is there any way to use the watchOS 3 new functionality without an iPhone or a server? What if we want the watchOS to schedule the background tasks? How do you do this without using WKWatchConnectivityRefreshBackgroundTask or WKURLSessionRefreshBackgroundTask? Say that the model data is updating constantly on the watch, independently of a paired iPhone. How would you trigger that and then schedule a background task, directly from watchOS methods with no knowledge of the paired phone?Vicious
Very true. In fact in watchOS 4 using background refresh app tasks is mandatory over the old requestedUpdateDidBegin() way. Could you post an example on how to implement background refresh app tasks to bkwebhero's code?Bludge

© 2022 - 2024 — McMap. All rights reserved.