greg
greg

Reputation: 4953

WKWatchConnectivityRefreshBackgroundTask competing with WCSessionDelegate

I'm trying to adapt my code from using only WCSessionDelegate callbacks in the foreground to accepting WKWatchConnectivityRefreshBackgroundTask via handleBackgroundTasks: in the background. The documentation states that background tasks may come in asynchronously and that one should not call setTaskCompleted until the WCSession's hasContentPending is NO.

If I put my watch app into the background and transferUserInfo: from an iPhone app, I am able to successfully receive my first WKWatchConnectivityRefreshBackgroundTask. However, hasContentPending is always YES, so I save away the task and simply return from my WCSessionDelegate method. If I transferUserInfo: again, hasContentPending is NO, but there is no WKWatchConnectivityRefreshBackgroundTask associated with this message. That is, subsequent transferUserInfo: do not trigger a call to handleBackgroundTask: –- they're simply handled by the WCSessionDelegate. Even if I immediately setTaskCompleted without checking hasContentPending, subsequent transferUserInfo: are handled by session:didReceiveUserInfo: without me needing to activate a WCSession again.

I'm not sure what to do here. There doesn't seem to be a way to force a WCSession to deactivate, and following the documentation about delaying setTaskCompleted seems to be getting me into trouble with the OS.

I've posted and documented a sample project illustrating my workflow on GitHub, pasting my WKExtensionDelegate code below. Am I making an incorrect choice or interpreting the documentation incorrectly somewhere along the line?

I've looked at the QuickSwitch 2.0 source code (after fixing the Swift 3 bugs, rdar://28503030), and their method simply doesn't seem to work (there's another SO thread about this). I've tried using KVO for WCSession's hasContentPending and activationState, but there's still never any WKWatchConnectivityRefreshBackgroundTask to complete, which makes sense to be given my current explanation of the issue.

#import "ExtensionDelegate.h"

@interface ExtensionDelegate()

@property (nonatomic, strong) WCSession *session;
@property (nonatomic, strong) NSMutableArray<WKWatchConnectivityRefreshBackgroundTask *> *watchConnectivityTasks;

@end

@implementation ExtensionDelegate

#pragma mark - Actions

- (void)handleBackgroundTasks:(NSSet<WKRefreshBackgroundTask *> *)backgroundTasks
{
    NSLog(@"Watch app woke up for background task");

    for (WKRefreshBackgroundTask *task in backgroundTasks) {
        if ([task isKindOfClass:[WKWatchConnectivityRefreshBackgroundTask class]]) {
            [self handleBackgroundWatchConnectivityTask:(WKWatchConnectivityRefreshBackgroundTask *)task];
        } else {
            NSLog(@"Handling an unsupported type of background task");
            [task setTaskCompleted];
        }
    }
}

- (void)handleBackgroundWatchConnectivityTask:(WKWatchConnectivityRefreshBackgroundTask *)task
{
    NSLog(@"Handling WatchConnectivity background task");

    if (self.watchConnectivityTasks == nil)
        self.watchConnectivityTasks = [NSMutableArray new];
    [self.watchConnectivityTasks addObject:task];

    if (self.session.activationState != WCSessionActivationStateActivated)
        [self.session activateSession];
}

#pragma mark - Properties

- (WCSession *)session
{
    NSAssert([WCSession isSupported], @"WatchConnectivity is not supported");

    if (_session != nil)
        return (_session);

    _session = [WCSession defaultSession];
    _session.delegate = self;

    return (_session);
}

#pragma mark - WCSessionDelegate

- (void)session:(WCSession *)session activationDidCompleteWithState:(WCSessionActivationState)activationState error:(NSError *)error
{
    switch(activationState) {
        case WCSessionActivationStateActivated:
            NSLog(@"WatchConnectivity session activation changed to \"activated\"");
            break;
        case WCSessionActivationStateInactive:
            NSLog(@"WatchConnectivity session activation changed to \"inactive\"");
            break;
        case WCSessionActivationStateNotActivated:
            NSLog(@"WatchConnectivity session activation changed to \"NOT activated\"");
            break;
    }
}

- (void)sessionWatchStateDidChange:(WCSession *)session
{
    switch(session.activationState) {
        case WCSessionActivationStateActivated:
            NSLog(@"WatchConnectivity session activation changed to \"activated\"");
            break;
        case WCSessionActivationStateInactive:
            NSLog(@"WatchConnectivity session activation changed to \"inactive\"");
            break;
        case WCSessionActivationStateNotActivated:
            NSLog(@"WatchConnectivity session activation changed to \"NOT activated\"");
            break;
    }
}

- (void)session:(WCSession *)session didReceiveUserInfo:(NSDictionary<NSString *, id> *)userInfo
{
    /*
     * NOTE:
     * Even if this method only sets the task to be completed, the default
     * WatchConnectivity session delegate still picks up the message
     * without another call to handleBackgroundTasks:
     */

    NSLog(@"Received message with counter value = %@", userInfo[@"counter"]);

    if (session.hasContentPending) {
        NSLog(@"Task not completed. More content pending...");
    } else {
        NSLog(@"No pending content. Marking all tasks (%ld tasks) as complete.", (unsigned long)self.watchConnectivityTasks.count);
        for (WKWatchConnectivityRefreshBackgroundTask *task in self.watchConnectivityTasks)
            [task setTaskCompleted];
        [self.watchConnectivityTasks removeAllObjects];
    }
}

@end

Upvotes: 0

Views: 1220

Answers (1)

ccjensen
ccjensen

Reputation: 4656

From your description and my understanding it sounds like this is working correctly.

The way this was explained to me is that the new handleBackgroundTasks: on watchOS is intended to be a way for:

  • the system to communicate to the WatchKit extension why it is being launched/resumed in the background, and
  • a way for the WatchKit extension to let the system know when it has completed the work it wants to do and can therefore be terminated/suspended again.

This means that whenever an incoming WatchConnectivity payload is received on the Watch and your WatchKit extension is terminated or suspended you should expect one handleBackgroundTasks: callback letting you know why you are running in the background. This means you could receive 1 WKWatchConnectivityRefreshBackgroundTask but several WatchConnectivity callbacks (files, userInfos, applicationContext). The hasContentPending lets you know when your WCSession has delivered all of the initial, pending content (files, userInfos, applicationContext). At that point, you should call the setTaskCompleted on the WKWatchConnectivityRefreshBackgroundTask object.

Then you can expect that your WatchKit extension will soon thereafter be suspended or terminated unless you have received other handleBackgroundTasks: callbacks and therefore have other WK background task objects to complete.

I have found that when attaching to the processes with a debugger that OSs might not suspended them like they normally would, so it'd suggest inspecting the behavior here using logging if you want to be sure to avoid any of those kind of issues.

Upvotes: 1

Related Questions