Can't figure out Complications and how to update..

Hi all, I'm trying to solve a very simple problem - I want to get data from the web every hour and then update my complications. That's all.

However, in practice this seems very difficult? I can't find a reliable way to actually update any data. Whether it be `getNextRequestedUpdateDate` not being called or `requestedUpdateDidBegin` not being called after scheduling an update..

I've tried using Background tasks also which have been a bit of a mess to say the least. I opted not to use them because of how bad the API was to implement them.

Has anyone found a worthwhile tutorial or code snippet to actually explain how to do this very simple procedure?

Here is some of my sample code

func requestedUpdateDidBegin() {
    print("Starting to update complication")
    if ComplicationWebService.sharedInstance.shouldRefresh() {
      ComplicationWebService.sharedInstance.getPersonalAgenda() // This gets the data from the web. Upon completion it will call CLKComplicationServer timeline refresh
    } else {
      ComplicationController.refresh() // This reloads the timelines of the complications using CLKComplicationServer
    }
  }

  func requestedUpdateBudgetExhausted() {
    print("Budget exhausted!")
  }

  func getNextRequestedUpdateDate(handler: @escaping (Date?) -> Void) {
    let nextUpdateDate = Date().dateByAddingMinutes(ComplicationWebService.defaultRefreshInterval) // Set to 30 minutes, 10 mins when testing
    print("Getting next requested update date: \(nextUpdateDate)")
    handler(nextUpdateDate)
  }

Accepted Reply

Even though Background Tasks are what Apple pushes as a preferred way to update the complication and the app in the background, I too found them far more convoluted to implement, and even when I did, I realized I can't update as frequently as I could with the getNextRequestedUpdateDate approach. For what you need (update complication only), your approach is indeed far easier and more straightforward. Your code looks OK, are you sure you deleted the


func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {


from the ExtensionDelegate? If you have that function there, the code you have will be ignored, as you can only have one or the other (background tasks using handle, or the getNextRequestedUpdateDate for complications update. Are you sure you have all the other required functions in the ComplicationController? If you want, you can upload your code somewhere and I'll take a look, or I can send you a working code sample, whichever you prefer.

Replies

Even though Background Tasks are what Apple pushes as a preferred way to update the complication and the app in the background, I too found them far more convoluted to implement, and even when I did, I realized I can't update as frequently as I could with the getNextRequestedUpdateDate approach. For what you need (update complication only), your approach is indeed far easier and more straightforward. Your code looks OK, are you sure you deleted the


func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {


from the ExtensionDelegate? If you have that function there, the code you have will be ignored, as you can only have one or the other (background tasks using handle, or the getNextRequestedUpdateDate for complications update. Are you sure you have all the other required functions in the ComplicationController? If you want, you can upload your code somewhere and I'll take a look, or I can send you a working code sample, whichever you prefer.

Hey thanks for your reply. I actually didn't even realise that the handle delegate method would block the data source's implemented methods! Removed that and everything works (sort of) for now. The updates don't exactly happen every 30 mins - hour but it updates somewhat so I'm happy. It would be nicer to have a better API for scheduling complication updates. The new way seems absolutely absurd.

Hi Marconelly,


I have been experimenting with watchOS 3 as well as and agree with your assessment that the background tasks method is convoluted, unclear as to how to enforce it. If you are happy with the method you've chosen, can you share or send some sample code in objective-C? (Swift if you don't have any obj-C code) I'm trying a simple method of updating a step counter complication. (CMPedometer data beind stored inside a model, and then the # of steps left being shown on the complication) My main issue is enforcing proper refresh. (Giving it a whole timeline in advance is sort of meaningless for a step counter unless the person is in motion)


Thank you in advance!


halordain - at - gmail

For sure, I can share the code sample. As you only need to update the complication, I assume you only need a code sample for getNextRequestedUpdateDate method (which is capable of updating the complication in the background, but not the main app) So it will just be a variation of what Chackle1337h4x0r already posted, only in Objective C. So again, first make sure to delete the handleBackgroundTasks method from the extension delegate class. If you don't, none of this will work. Here's the full ComplicationController.m code from the small test app I just made. All it does is every 10 minutes it updates the time on a large modular complication. So it will essentially show you the time of the last complication update. It's commented, so you can see where you need to insert your data update code. I amrunning this code on the simulator now, and it's working fine, updating every 10 minutes as expected. Let me know if you need anything more.


#import "ComplicationController.h"

@interface ComplicationController ()
@end

@implementation ComplicationController

NSString *strCurrentTime = @"";

#pragma mark - Timeline Configuration
- (void)getSupportedTimeTravelDirectionsForComplication:(CLKComplication *)complication withHandler:(void(^)(CLKComplicationTimeTravelDirections directions))handler {
    handler(CLKComplicationTimeTravelDirectionForward|CLKComplicationTimeTravelDirectionBackward);
}

- (void)getTimelineStartDateForComplication:(CLKComplication *)complication withHandler:(void(^)(NSDate * __nullable date))handler {
    handler(nil);
}

- (void)getTimelineEndDateForComplication:(CLKComplication *)complication withHandler:(void(^)(NSDate * __nullable date))handler {
    handler(nil);
}

- (void)getPrivacyBehaviorForComplication:(CLKComplication *)complication withHandler:(void(^)(CLKComplicationPrivacyBehavior privacyBehavior))handler {
    handler(CLKComplicationPrivacyBehaviorShowOnLockScreen);
}

#pragma mark - Timeline Population
// this method is called when the updateComplicationServer method reloads timeline for the active complication.
- (void)getCurrentTimelineEntryForComplication:(CLKComplication *)complication withHandler:(void(^)(CLKComplicationTimelineEntry * __nullable))handler {
  NSDate *now = [NSDate date];
  if (complication.family == CLKComplicationFamilyModularLarge) {
       CLKComplicationTemplateModularLargeTallBody *complicationTemplate = [[CLKComplicationTemplateModularLargeTallBody alloc] init];
       complicationTemplate.headerTextProvider = [CLKSimpleTextProvider textProviderWithText:@"Last Refresh Time:"];
       complicationTemplate.bodyTextProvider = [CLKSimpleTextProvider textProviderWithText:strCurrentTime];
       CLKComplicationTimelineEntry *entry = [CLKComplicationTimelineEntry entryWithDate:now complicationTemplate:complicationTemplate];
       handler(entry);
  } else {
       handler(nil);
  }
}
- (void)getTimelineEntriesForComplication:(CLKComplication *)complication beforeDate:(NSDate *)date limit:(NSUInteger)limit withHandler:(void(^)(NSArray<CLKComplicationTimelineEntry *> * __nullable entries))handler {
    handler(nil);
}
- (void)getTimelineEntriesForComplication:(CLKComplication *)complication afterDate:(NSDate *)date limit:(NSUInteger)limit withHandler:(void(^)(NSArray<CLKComplicationTimelineEntry *> * __nullable entries))handler {
    handler(nil);
}
#pragma mark - Placeholder Templates
- (void)getLocalizableSampleTemplateForComplication:(CLKComplication *)complication withHandler:(void(^)(CLKComplicationTemplate * __nullable complicationTemplate))handler {
    handler(nil);
}
- (void)getNextRequestedUpdateDateWithHandler:(void(^)(NSDate * __nullable updateDate))handler {
  // Call the handler with the date when you would next like to be given the opportunity to update your complication content
  NSLog(@"clock:getNextRequestedUpdateDateWithHandler");

  handler([NSDate dateWithTimeIntervalSinceNow: (60 * 9)]); // every 10 minutes is the shortest allowed update time, so set this at slightly less than that to ensure it fires every ten minutes
}
- (void)requestedUpdateDidBegin {
  //Not shown: decide if you actually need to update your complication.
  //If you do, execute the following code:

  NSLog(@"clock:requestedUpdateDidBegin");
  // read the number of steps here, or otherwise update your data.
  // When done, call the updateComplicationServer method.
  // in case of this example I will just read current time and format it as a string,
  // then store it in a variable to be used in the getCurrentTimelineEntryForComplication method.
  NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  [dateFormatter setDateFormat:@"HH:mm:ss"];
  strCurrentTime = [dateFormatter stringFromDate:[NSDate date]];
  [self updateComplicationServer];
}
- (void)requestedUpdateBudgetExhausted {
  NSLog(@"clock:requestedUpdateBudgetExhausted");
}
- (void)updateComplicationServer {
  for (CLKComplication *comp in [CLKComplicationServer sharedInstance].activeComplications) {
     [[CLKComplicationServer sharedInstance] reloadTimelineForComplication:comp];
  }
}
@end

Thank you so much, Marconelly. Your code for RequestedUpdate... helped me at least see a refresh on schedule.


So you haven't needed or found significant advantage in using the background tasks to handle on-watch complication updates? (specific example: walk a few steps, and after 100 steps, watchOS 3 runs a background task to force a complication refresh?)

Not a problem, let me know if you need more help.


The advantage of using Background Tasks is that you can update the actual app in the background, and take a snapshot after the update, so the app will show the last updated data with no visible jump from old data -> new data when you see it in the dock or start it. The big drawback of background tasks however (other than incredibly frustrating and poorly documented implementation - not even Apple's demo project for this works right) is that it's not possible to get updates as frequently as you can with getNextRequestedUpdate complication method. With getNextRequestedUpdate, you can get updates every ten minutes, never skipping a beat, if the complication is on the active watch face. To update the app itself, you have to wait for the app to launch or become visible in the dock, then apply the data you retrieved by the complication. So you get a quick jump from old state -> new state this way, just like the Activity rings app does when you scroll to it in the dock.


With background tasks, if you have complication on the active watch face, you can typically get updates every 20-30 minutes. The problem is that background tasks work in two cycles - background data download, which repeats about every 15 minutes, and background app update, which also runs at about 15 minute intervals, using data downloaded during the background data download cycle. These intervals are not necessarily synced though, so you'll get visible app updates about every 20 minutes only if you're lucky, and it can never be less than 15 minutes.


Keep in mind that with either of these two methods you cannot really have a trigger in your code to run the background task at the exact time you want. So your example of triggering a background task after every 100 steps is not really possible. The best you can do is display how many steps have been taken every 10 minutes.

Although I can't find any documentation that actually states this, it appears that in watchOS4 the getNextRequestedUpdateDate simply (silently) fails to run, as a result of which if you simply recompile an older app under iOS11 your complications will now just fail.


Hence if you wish to use iOS11 then you apparently now MUST implement the background refresh methods for complications. Having spent nearly two full weeks attempting to debug and work around the many glitches and major inconsistencies between how these run on actual device versus simulator, incorrrect Apple sample code and documentation and STILL not managed to get a properly working set of complications (they worked perfectly under the old model), I have to agree entirely with you that the background task implementation is an utter can of worms and to be avoided at all possible costs.


Worst glitches -

1) WKURLSessionRefreshBackgroundTask is NEVER called when you run the simulator. Hence you have little chocie other than to do all testing/debugging on an actual watch

2) XCode support for debugging on actual watch is very flakey, often fails to attach to process. Extremely slow to start up when it does.

3) WKApplicationRefreshBackgroundTask must be scheduled with "preferred" time with no precise control over when it fires, even if just for testing - I've found myself sitting 30mins and longer just waiting for a refresh to fire to test one small section of code. I can only presume (?) I've exhausted some invisible budget. Or is this some other kind of XCode or API bug?

4) If instead of running WKURLSessionRefreshBackgroundTask you try to run your own asynchronous task, you risk being silently terminated, with no way to run clean-up code. In my own testing I found that after about 60 correct runs of my asynchronous task, the next several were all silently terminated, thus throwing my code into unpredicatble state and thus blocking further updates.


If anyone has any comments or wants to suggest that I've got any of this wrong at all, I'd love to hear it.

Everything you wrote here is pretty much spot-on. Old method of complication updates fails now (actually if you look at the debug output, XCode does tell you that getNextRequestedUpdateDateWithHandler is no longer called under watchOS4. All your other comments are true as well. The background update process download task simply doesn't work on the simulator (never did, and still doesn't in XCode 9). It can be scheduled, but it never fires on simulator. Debugging must be done on a real watch, but I've found out that when you set the preferred time 1 minute in the future, it typically updates every 7-8 minutes under watchOS4. It appears to be very reliable, much more so than on watchOS3. Also, yes, Apple's sample code used to be incorrect last I checked it - at the very least it didn't store the download task into a variable, that needs to be set as complete only after the download has actually been completed. This needs to be handled with a lot care - make absolutely sure to not set the same task as complete twice by mistake, as that will either crash the app, or stop further background updates - I can't remember now.


I assume they got rid of the old complication update method as it could get abused. It was possible to initiate downloads at will, and do other things when the update interval comes around, which if not done correctly, could go haywire with some poorly written code, and drain the battery. Still, doesn't excuse how poorly documented and poorly explained the background update method is in comparison.

I don't have it in front of me, but at one point the release notes for one WatchOS beta had buried a trick in which you could trigger the background refresh in the simulator from Xcode's menu, from a somewhat unintuitively-named task. Wish I could remember, but worth hunting around for.

By any chance, did you manage to remember how? I'm scratching my head with this. I'd really like to force the simulator to send tasks to my Watch app. I have looked at all the options available and I can't for the life of me figure it out.

Marconelly, I had complication with timeline data... used to work, then background task I have not gotten to work. I have to restart watch then the data is there... I emailed appel support to make use of my 2 helps, but they jsut gave me WWDC links.


Glad to sahre the code so you can see help please, be much apprecaited.


can we exchange emails directly please? mike@derr.ws is me.


thank you so much, look forward to hearing from you. Hope you see this, lol.


Regards, Mike