27

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?

mfaani
  • 33,269
  • 19
  • 164
  • 293
sash
  • 8,423
  • 5
  • 63
  • 74

3 Answers3

43

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
sash
  • 8,423
  • 5
  • 63
  • 74
  • 2
    This doesn't seem to work and is very difficult to understand. `getUserLocationWithInterval` is never called? Please post a *working* example. – pcoving Sep 30 '13 at 15:54
  • @pcoving You have to create instance of ScheduledLocationManager and call getUserLocationWithInterval passing an interval in seconds. – sash Sep 30 '13 at 16:28
  • Will this work if you create an instance of ScheduledLocationManager in background? I'm trying to start it in application:didFinishLaunchingWithOptions: with locationKey after waking up from regions monitoring or signifficantLocationChange. It doesn't seem to work – Lena Grushevsky Oct 16 '13 at 09:41
  • 1
    Do you create an instance in App Delegate? – Taylor Abernethy Newman Oct 16 '13 at 20:29
  • can any tell me how to use this example?? any sourcecode will be great. –  Oct 25 '13 at 05:42
  • 1
    Hi I got this error while using above example code `-[__NSDictionaryI applicationDidBecomeActive:]: unrecognized selector sent to instance 0x14db5cf0` Please give some direction regarding it :) – The iOSDev Dec 12 '13 at 10:25
  • Also this one some times if i debug the application `-[NSConcreteNotification applicationDidBecomeActive:]: unrecognized selector sent to instance 0x165ba320` Any thing in solving of this error :) – The iOSDev Dec 12 '13 at 10:53
  • 3
    This is perfectly working and just save my hours of R&D Thanks for that and +1 also :) thanks for posting this solution @sash – The iOSDev Dec 13 '13 at 05:07
  • Last time when I tried this solutuon, it doesn't work well on my end. After months of testing, I have found a good solution here: http://stackoverflow.com/questions/18946881/background-location-services-not-working-in-ios-7/21966662#21966662 If you have any question, you are welcomed to join us for a discussion here: http://mobileoop.com/background-location-update-programming-for-ios-7 – Ricky May 01 '14 at 15:10
  • @sash I tried the above code and I am getting the warning "Can't endBackgroundTask: no background task exists with identifier 4, or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug." And after some time my app crashed saying "Terminated due to Memory Error". Can you please tell how did you declare UIBackgroundTaskIdentifier in your code ? – Sharme May 26 '14 at 06:53
  • I've done some testing. It seems like the time limit of the background task is insignificant when you have added location to the `UIBackgroundModes`...? I have now been running 70x30 seconds (still counting (and I'm of course not going to do this in production)) scheduled location updates without "renewing" the background task. `[[UIApplication sharedApplication]backgroundTimeRemaining]` is now returning zero, but the background process continues. Could it be that the `backgroundTimeRemaining` is ignored when you have added the `UIBackgroundModes` plist property? – Elechtron Jun 11 '14 at 10:06
  • 1
    @AskeAnker background process will stop when you will make a release build. – sash Jun 11 '14 at 13:31
  • @sash Does this method works even if app is removed from background? – Azhar Bandri Jul 11 '14 at 12:21
  • Why shouldn't the interval be higher than kMaxBGTime? Can't it be set to like 15 minutes? – SleepNot Jul 13 '14 at 12:37
  • @jeraldo No, you can't magically go up to 15 minutes, as the OS only allows you to execute for `[[UIApplication sharedApplication] backgroundTimeRemaining]` which, as mentioned at the top of this answer, is only a max of 3 minutes as of iOS7. – u2Fan Jul 28 '14 at 16:58
  • you can see location tracker sample at this link , https://github.com/voyage11/Location – Nada Gamal Oct 13 '14 at 12:36
  • There should be code to stop ScheduledLocationManager. Because once, it started, it will never stop at all. OR May be we have to dealloc this object. – Asif Bilal Dec 31 '14 at 16:24
  • @sash, Your solution is perfectly working till 'iOS8', but it seems that in 'iOS9', background service stops after app goes in background, and doesn't start timer again. Can you plz help asap. – Viral Savaj Nov 16 '15 at 10:51
  • @ViralSavaj to get it work on iOS 9 add `locationManager.allowsBackgroundLocationUpdates = YES;` to initializer and remove this line `[self performSelector:@selector(stopBackgroundTask) withObject:nil afterDelay:1];` from `timerEvent` function. Hope it will help someone like me :D – mkz Apr 08 '16 at 11:57
  • @sash, thanks for the great input. Can you help me resolving a follow up question: how the battery consumption would be with this ScheduledLocationManager? – makeasy Feb 22 '17 at 22:33
  • It all depends on frequency interval and accuracy. Setting higher accuracy will trigger iOS device to use GPS. https://developer.apple.com/reference/corelocation/core_location_constants/accuracy_constants – sash Feb 23 '17 at 11:50
7

I tried your method but it didn't work on my side. Can you show me your code?

I actually found a solution to solve the location service problem in iOS 7.

In iOS 7, you can not start the location service in background. If you want the location service to keep running in the background, you have to start it in foreground and it will continue to run in the background.

If you were like me, stop the location service and use timer to re-start it in the background, it will NOT work in iOS 7.

For more detailed information, you can watch the first 8 minutes of video 307 from WWDC 2013: https://developer.apple.com/wwdc/videos/

Update: The location service can work in background as well. Please check Background Location Services not working in iOS 7 for the updated post with complete solution posted on Github and a blog post explaining the details.

Community
  • 1
  • 1
Ricky
  • 10,485
  • 6
  • 36
  • 49
  • Hi there, how can I start the location services in the foreground? I'm having the same problem. Thanks. – Guerrix Oct 23 '13 at 12:52
  • Hi Guerrix, you may see my full solution here [iOS 7 Background Service](http://stackoverflow.com/questions/18946881/background-location-services-not-working-in-ios-7/21966662#21966662) – Ricky Feb 23 '14 at 09:51
  • @Ricky what can be done if we want to get the location updates even if the app is removed from background through double tap on home screen? – Azhar Bandri Jul 11 '14 at 11:52
  • @Azhar I heard that it is possible to send the location update even when the app is killed (not on foreground nor background) since iOS 7.1 but I still havent find any reliable solution yet. – Ricky Jul 12 '14 at 12:58
  • You said that the location service must be started in the foreground? What if after enter background, I have to get the user's location every after 15 minutes? – SleepNot Jul 13 '14 at 12:40
  • @jeraldo This is an old post. I have posted a solution with a detail blog post and a completed solution on Github on how to make location service works in Background here: http://stackoverflow.com/questions/18946881/background-location-services-not-working-in-ios-7/21966662#21966662 Please upvote that post if it helps you. – Ricky Jul 14 '14 at 00:48
6

Steps to get this implemented are as follows:

  1. Add "App registers for location updates" at item 0 in "Required background modes" in info.plist of your project.

  2. Write below code at application did finish launching.

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(startFetchingLocationsContinously) name:START_FETCH_LOCATION object:nil];
    
  3. Write below code from where you want to start tracking

    [[NSNotificationCenter defaultCenter] postNotificationName:START_FETCH_LOCATION object:nil];
    
    AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
                [appDelegate startUpdatingDataBase];
    
  4. Paste following code to AppDelegate.m

    #pragma mark - Location Update
    -(void)startFetchingLocationsContinously{
        NSLog(@"start Fetching Locations");
        self.locationUtil = [[LocationUtil alloc] init];
        [self.locationUtil setDelegate:self];
        [self.locationUtil startLocationManager];
    }
    
    -(void)locationRecievedSuccesfullyWithNewLocation:(CLLocation*)newLocation oldLocation:(CLLocation*)oldLocation{
        NSLog(@"location received successfullly in app delegate for Laitude: %f and Longitude:%f, and Altitude:%f, and Vertical Accuracy: %f",newLocation.coordinate.latitude,newLocation.coordinate.longitude,newLocation.altitude,newLocation.verticalAccuracy);
    }
    
    -(void)startUpdatingDataBase{
        UIApplication*    app = [UIApplication sharedApplication];
    
        bgTask = UIBackgroundTaskInvalid;
    
        bgTask = [app beginBackgroundTaskWithExpirationHandler:^(void){
            [app endBackgroundTask:bgTask];
        }];
    
        SAVE_LOCATION_TIMER =  [NSTimer scheduledTimerWithTimeInterval:300
                                                                target:self selector:@selector(startFetchingLocationsContinously) userInfo:nil repeats:YES];
    }
    
  5. Add a class by name "LocationUtil" and paste following code into the header file:

    #import <Foundation/Foundation.h>
    #import <CoreLocation/CoreLocation.h>
    @protocol LocationRecievedSuccessfully <NSObject>
    @optional
    -(void)locationRecievedSuccesfullyWithNewLocation:(CLLocation*)newLocation oldLocation:(CLLocation*)oldLocation;
    -(void)addressParsedSuccessfully:(id)address;
    
    @end
    @interface LocationUtil : NSObject <CLLocationManagerDelegate> {
    }
    
    //Properties
    @property (nonatomic,strong) id<LocationRecievedSuccessfully> delegate;
    -(void)startLocationManager;
    

    And paste following code in LocationUtil.m

    -(void)startLocationManager{
    
         locationManager = [[CLLocationManager alloc] init];
         locationManager.delegate = self;
         [locationManager setPausesLocationUpdatesAutomatically:YES]; //Utkarsh 20sep2013
         //[locationManager setActivityType:CLActivityTypeFitness];
         locationManager.distanceFilter = kCLDistanceFilterNone;
         locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation;
         [locationManager startUpdatingLocation];
    
         //Reverse Geocoding.
         geoCoder=[[CLGeocoder alloc] init];
    
        //set default values for reverse geo coding.
    }
    
    //for iOS<6
    - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
      //call delegate Method
      [delegate locationRecievedSuccesfullyWithNewLocation:newLocation oldLocation:oldLocation];
    
      NSLog(@"did Update Location");
    }
    
    //for iOS>=6.
    
    - (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
    
      CLLocation *newLocation = [locations objectAtIndex:0];
      CLLocation *oldLocation = [locations objectAtIndex:0];
    
      [delegate locationRecievedSuccesfullyWithNewLocation:newLocation oldLocation:oldLocation];
      NSLog(@"did Update Locationsssssss");
    }
    
Meet Doshi
  • 4,241
  • 10
  • 40
  • 81
Utkarsh Goel
  • 245
  • 2
  • 5