Search code examples
iosobjective-cios7core-locationcllocationmanager

Start Location Manager in iOS 7 from background task


It seems that in iOS 7 an app can not start Location Manager (by calling startUpdatingLocation) from the background task anymore.

In iOS 6 I used approach described here: https://stackoverflow.com/a/6465280 to run background location update every n minutes. The idea was to run background task with a timer and start Location Manager when the timer triggers it. After that turn off Location Manager and start another background task.

After updating to iOS 7 this approach does not work anymore. After starting Location Manager an app does not receive any locationManager:didUpdateLocations. Any ideas?


Solution

  • I found the problem/solution. When it is time to start location service and stop background task, background task should be stopped with a delay (I used 1 second). Otherwise location service wont start. Also Location Service should be left ON for a couple of seconds (in my example it is 3 seconds).

    Another important notice, max background time in iOS 7 is now 3 minutes instead of 10 minutes.

    Updated on October 29 '16

    There is a cocoapod APScheduledLocationManager that allows to get background location updates every n seconds with desired location accuracy.

    let manager = APScheduledLocationManager(delegate: self)
    manager.startUpdatingLocation(interval: 170, acceptableLocationAccuracy: 100)
    

    The repository also contains an example app written in Swift 3.

    Updated on May 27 '14

    Objective-C example:

    1) In ".plist" file set UIBackgroundModes to "location".

    2) Create instance of ScheduledLocationManager anywhere you want.

    @property (strong, nonatomic) ScheduledLocationManager *slm;
    

    3) Set it up

    self.slm = [[ScheduledLocationManager alloc]init];
    self.slm.delegate = self;
    [self.slm getUserLocationWithInterval:60]; // replace this value with what you want, but it can not be higher than kMaxBGTime
    

    4) Implement delegate methods

    -(void)scheduledLocationManageDidFailWithError:(NSError *)error
    {
        NSLog(@"Error %@",error);
    }
    
    -(void)scheduledLocationManageDidUpdateLocations:(NSArray *)locations
    {
        // You will receive location updates every 60 seconds (value what you set with getUserLocationWithInterval)
        // and you will continue to receive location updates for 3 seconds (value of kTimeToGetLocations).
        // You can gather and pick most accurate location
        NSLog(@"Locations %@",locations);
    }
    

    Here is implementation of ScheduledLocationManager:

    ScheduledLocationManager.h

    #import <Foundation/Foundation.h>
    #import <CoreLocation/CoreLocation.h>
    
    @protocol ScheduledLocationManagerDelegate <NSObject>
    
    -(void)scheduledLocationManageDidFailWithError:(NSError*)error;
    -(void)scheduledLocationManageDidUpdateLocations:(NSArray*)locations;
    
    @end
    
    @interface ScheduledLocationManager : NSObject <CLLocationManagerDelegate>
    
    -(void)getUserLocationWithInterval:(int)interval;
    
    @end
    

    ScheduledLocationManager.m

    #import "ScheduledLocationManager.h"
    
    int const kMaxBGTime = 170; // 3 min - 10 seconds (as bg task is killed faster)
    int const kTimeToGetLocations = 3; // time to wait for locations
    
    @implementation ScheduledLocationManager
    {
        UIBackgroundTaskIdentifier bgTask;
        CLLocationManager *locationManager;
        NSTimer *checkLocationTimer;
        int checkLocationInterval;
        NSTimer *waitForLocationUpdatesTimer;
    }
    
    - (id)init
    {
        self = [super init];
        if (self) {
            locationManager = [[CLLocationManager alloc] init];
            locationManager.delegate = self;
            locationManager.desiredAccuracy = kCLLocationAccuracyBest;
            locationManager.distanceFilter = kCLDistanceFilterNone;
    
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
        }
        return self;
    }
    
    -(void)getUserLocationWithInterval:(int)interval
    {
        checkLocationInterval = (interval > kMaxBGTime)? kMaxBGTime : interval;
        [locationManager startUpdatingLocation];
    }
    
    - (void)timerEvent:(NSTimer*)theTimer
    {
        [self stopCheckLocationTimer];
        [locationManager startUpdatingLocation];
    
        // in iOS 7 we need to stop background task with delay, otherwise location service won't start
        [self performSelector:@selector(stopBackgroundTask) withObject:nil afterDelay:1];
    }
    
    -(void)startCheckLocationTimer
    {
        [self stopCheckLocationTimer];
        checkLocationTimer = [NSTimer scheduledTimerWithTimeInterval:checkLocationInterval target:self selector:@selector(timerEvent:) userInfo:NULL repeats:NO];
    }
    
    -(void)stopCheckLocationTimer
    {
        if(checkLocationTimer){
            [checkLocationTimer invalidate];
            checkLocationTimer=nil;
        }
    }
    
    -(void)startBackgroundTask
    {
        [self stopBackgroundTask];
        bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
            //in case bg task is killed faster than expected, try to start Location Service
            [self timerEvent:checkLocationTimer];
        }];
    }
    
    -(void)stopBackgroundTask
    {
        if(bgTask!=UIBackgroundTaskInvalid){
            [[UIApplication sharedApplication] endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }
    }
    
    -(void)stopWaitForLocationUpdatesTimer
    {
        if(waitForLocationUpdatesTimer){
            [waitForLocationUpdatesTimer invalidate];
            waitForLocationUpdatesTimer =nil;
        }
    }
    
    -(void)startWaitForLocationUpdatesTimer
    {
        [self stopWaitForLocationUpdatesTimer];
        waitForLocationUpdatesTimer = [NSTimer scheduledTimerWithTimeInterval:kTimeToGetLocations target:self selector:@selector(waitForLoactions:) userInfo:NULL repeats:NO];
    }
    
    - (void)waitForLoactions:(NSTimer*)theTimer
    {
        [self stopWaitForLocationUpdatesTimer];
    
        if(([[UIApplication sharedApplication ]applicationState]==UIApplicationStateBackground ||
            [[UIApplication sharedApplication ]applicationState]==UIApplicationStateInactive) &&
           bgTask==UIBackgroundTaskInvalid){
            [self startBackgroundTask];
        }
    
        [self startCheckLocationTimer];
        [locationManager stopUpdatingLocation];
    }
    
    #pragma mark - CLLocationManagerDelegate methods
    
    - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
    {
        if(checkLocationTimer){
            //sometimes it happens that location manager does not stop even after stopUpdationLocations
            return;
        }
    
        if (self.delegate && [self.delegate respondsToSelector:@selector(scheduledLocationManageDidUpdateLocations:)]) {
            [self.delegate scheduledLocationManageDidUpdateLocations:locations];
        }
    
        if(waitForLocationUpdatesTimer==nil){
            [self startWaitForLocationUpdatesTimer];
        }
    }
    
    - (void)locationManager:(CLLocationManager *)manager didFailWithError:(NSError *)error
    {
        if (self.delegate && [self.delegate respondsToSelector:@selector(scheduledLocationManageDidFailWithError:)]) {
            [self.delegate scheduledLocationManageDidFailWithError:error];
        }
    }
    
    #pragma mark - UIAplicatin notifications
    
    - (void)applicationDidEnterBackground:(NSNotification *) notification
    {
        if([self isLocationServiceAvailable]==YES){
            [self startBackgroundTask];
        }
    }
    
    - (void)applicationDidBecomeActive:(NSNotification *) notification
    {
        [self stopBackgroundTask];
        if([self isLocationServiceAvailable]==NO){
            NSError *error = [NSError errorWithDomain:@"your.domain" code:1 userInfo:[NSDictionary dictionaryWithObject:@"Authorization status denied" forKey:NSLocalizedDescriptionKey]];
    
            if (self.delegate && [self.delegate respondsToSelector:@selector(scheduledLocationManageDidFailWithError:)]) {
                [self.delegate scheduledLocationManageDidFailWithError:error];
            }
        }
    }
    
    #pragma mark - Helpers
    
    -(BOOL)isLocationServiceAvailable
    {
        if([CLLocationManager locationServicesEnabled]==NO ||
           [CLLocationManager authorizationStatus]==kCLAuthorizationStatusDenied ||
           [CLLocationManager authorizationStatus]==kCLAuthorizationStatusRestricted){
            return NO;
        }else{
            return YES;
        }
    }
    
    @end