Search code examples
iosbackgroundwatchkitlocation-services

Location Services not working in background called by WatchKit


My Apple Watch app requires some data and requests it from the corresponding iPhone app. To fulfill the request the iPhone app requires the users location. After receiving and testing with a real Apple Watch I found out that my iPhone app does not receive location updates when running in background. If the iPhone app is active in foreground it works without issues. With the simulator it worked in both cases.

In both cases (active and background) the WatchKit extension calls and starts successfully the iPhone app and goes all the way until startUpdatingLocation is called in the iPhone app. But in case the app is running in background didUpdateLocations is never called.

I tried with requestAlwaysAuthorization as well as requestWhenInUseAuthorization. No difference. I also activated then the "location updates" background mode within capabilities. But again no difference.

Has someone else faced the same problem and found a way to receive the location also in background?

Here some code. First the check if authorization is required.

// iOS 8 check to avoid crash on older iOS
    if ([self.locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)])
    {
        [self requestLocationAlwaysAuthorization];
    }
    else
    {
        [self runLocationUpdate];
    }

Here the check for the proper Location Manager rights.

- (void)requestLocationAlwaysAuthorization
{
CLAuthorizationStatus currentAuthStatus = [CLLocationManager authorizationStatus];

if (currentAuthStatus == kCLAuthorizationStatusDenied)
{
    //request user to change setting
}
else if (currentAuthStatus == kCLAuthorizationStatusRestricted)
{
    //request user to change setting
}
else if (currentAuthStatus == kCLAuthorizationStatusNotDetermined)
{
    [self.locationManager requestAlwaysAuthorization];

    [self runLocationUpdate];
}
else if (currentAuthStatus == kCLAuthorizationStatusAuthorizedWhenInUse)
{
    //maybe when in use is also enough?
    [self runLocationUpdate];
}
else if (currentAuthStatus == kCLAuthorizationStatusAuthorizedAlways)
{
    //all ok
    [self runLocationUpdate];
}

}

Here the call of startUpdatingLocation. The didUpdateLocations delegate will only be called when iPhone app is active.

-(void)runLocationUpdate
{
    [self.locationManager startUpdatingLocation];
}

Solution

  • Three things to check and be aware of:

    1. Location Permissions like [self.locationManager requestAlwaysAuthorization]; are only acknowledged once by the OS. If you have already requested permission, doesn't matter the level, the OS will NOT display a request to the user. The OS will just pass over the request and leave the permission level as is. The only time you can be assured that the OS will display the request to the user is if the [CLLocationManager authorizationStatus] returns kCLAuthorizationStatusNotDetermined. In every other case, you must manually request permission by displaying an Alert or other form of UI display. Also note that the OS retains whether or not it already displayed the request, even if you delete your app and reinstall it. So to test, you need to reset your Simulator's Content or your iPhone's Location Privacy.

    2. Make sure you have added the plist keys for NSLocationAlwaysUsageDescription AND NSLocationWhenInUseUsageDescription If you don't add this to your plist, the OS will ignore any Location Permission Requests.

    3. If you want to use requestAlwaysAuthorization to get location data from the phone (not the watch app extension) while the phone app is in the background, will also require you register for Background Modes Location updates under Project>Target>Capabilities.

    UPDATE Use a background task to give your app time to respond when in the background. Something like this:

    -(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply{
    
        UIApplication *app = [UIApplication sharedApplication];
    
        UIBackgroundTaskIdentifier bgTask __block = [app beginBackgroundTaskWithName:@"watchAppRequest" expirationHandler:^{
    
            [[UIApplication sharedApplication] endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
    
        }];
    
    //make your calls here to your tasks, when finished, send the reply then terminate the background task
    
    //send reply back to watch
    reply(replyInfo);
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
            [app endBackgroundTask:bgTask];
            bgTask=UIBackgroundTaskInvalid;
        });
    }