Reputation: 4953
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
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:
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