Rufen Sie uns an: Deutschland +49 30 23320715 - Österreich +43 1 3059571 | E-Mail: sales@creativeworkline.com
1

Core Location Manager in iOS 8 and fetching location in the background

Posted Dezember 5th, 2014 in Blog by creative workline

In the first part of this post we will discuss the changes that come to the CoreLocation Framework in iOS 8, and in the second part we will go over how to keep updating the app’s location in the background. So let’s begin!

Core Location Manager in iOS 8

The Core Location Framework in iOS worked over the years in almost the same way, in some version updates Apple may have changed the delegate methods, but all in all the process always stayed the same. Although the process in iOS 8 is not that different, Apple added two steps that might at first cause some trouble to developers who didn’t have a look into the new iOS 8 SDK.

Old Code behavior in iOS 8


- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // initialize the location manager
    self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;
    // Start getting location updates
    [self.locationManager startUpdatingLocation];

}

// Location updates
- (void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{
	// Do what ever you want with the location
}

Until iOS 8, this is how a simple location retrieving process would look like.
This code fails in iOS 8, furthermore this code fails without any sort of warnings, exceptions or errors, the app won’t ask for permission to get location updates, the process won’t start at all and we, the app developers won’t be told why.

Passing the code to iOS 8

As stated above, in iOS 8 they are new steps that we need to pay attention to in order make make the location fetching work. The first step is to add either one or two keys into the project’s .plist depending on the main functionality of the app. The two keys are NSLocationWhenInUseUsageDescription and NSLocationAlwaysUsageDescription, you will then need to add a String that explains to the user why does the app needs to access his location, something among the lines of “This app uses location in the background/foreground because of A, B and C”. Each of these Strings has a corresponding authorization method that needs to be called, WhenInUse or Alway (i.e. Background).

*Note: adding the keys without explicitly asking for authorization fails as well

Here are the corresponding methods:

[self.locationManager requestWhenInUseAuthorization]
[self.locationManager requestAlwaysAuthorization]

And here is a full implementation example in case of using the Always key String (i.e NSLocationAlwaysUsageDescription):

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.locationManager = [[CLLocationManager alloc] init];
    self.locationManager.delegate = self;
    if ([self.locationManager respondsToSelector:@selector(requestAlwaysAuthorization)])
    {
        [self.locationManager requestAlwaysAuthorization];
    }
    [self.locationManager startUpdatingLocation];
}

*Note: Don’t forget to add the key in the app’s .plist and to give it a string, otherwise the authorization UIAlertView won’t show and the process won’t start.

Choosing the right authorization

Now that we have to explicitly ask for the user’s permission, we also need to decide what kind of permission do we actually need so that we don’t ask for unnecessary permissions. As you probably already noticed, the keys and method names, give a pretty clear idea on should be the one or the other used. The WhenInUse key allows the app to receive location updates only when the app is in the foreground. The Always key allow the app to receive location updates at any given time in the background. For example if you would like at some point to wake the app based on the user’s location, you need the Always key, as working in the background is it’s main functionality. Of course, asking for the Always authorization gives the WhenInUse authorization, but not the other way around. If your app’s main functionality is getting location updates in the foreground but has a small functionality that should work in the background, you can use both keys and start by asking for the WhenInUse. You can always ask for the Always authorization at a later point, if needed (Keep in mind that once the user has accepted the one authorization, he won’t be asked again, and you need to ask him to go to the settings and change the authorization manually). All the location services are available with both authorizations, except that the WhenInUse authorization can access these services only when the app is in the foreground. If you wish to wake the app based on these services, you need to use the Always authorization.

Fetching location in the background

The following part of this article is based on the following example, provided by Ricky, here is the source code for those of you who are interested. After implementing everything needed to start receiving location updates, those who need the app to keep working and fetching the user’s location in the background, still need to face the challenge of keeping the app from terminating. Due to the iOS’s multitasking, the system will at some point move the app from running in background to suspended as it sees fit, at that point any process that the app was running will be stopped. In order to be able to consistently fetch location updates from the device, a solution must be implemented, that will consistently refresh the app in the background giving it enough time to perform a short time period process that will be executed with each refresh. In our case it’s to receive a location update and do whatever we want with it. Refreshing the app in the background is a matter of explicitly starting and ending background tasks through our [UIApplication sharedApplication] object and it’s two methods beginBackgroundTaskWithExpirationHandler: and endBackgroundTask:. Failing in one of these processes will cause the system to kill your app. In other words, in order for the process to keep on going, the starting and ending of the background tasks need to be synchronized with the start, stop and restart of the location fetching process.
But first things first, before starting any of the steps above, we must first check if the app has background authorization, as it might be that the user has disabled the background services:

UIAlertView * alert;

    //We have to make sure that the Background app Refresh is enabled for the Location updates to work in the background.
    if([[UIApplication sharedApplication] backgroundRefreshStatus] == UIBackgroundRefreshStatusDenied)
     {

            // The user explicitly disabled the background services for this app or for the whole system.

        alert = [[UIAlertView alloc]initWithTitle:@""
                                          message:@"The app doesn't work without the Background app Refresh enabled. To turn it on, go to Settings > General > Background app Refresh"
                                         delegate:nil
                                cancelButtonTitle:@"Ok"
                                otherButtonTitles:nil, nil];
        [alert show];

    } else if([[UIApplication sharedApplication] backgroundRefreshStatus] == UIBackgroundRefreshStatusRestricted)
    {

            // Background services are disabled and the user cannot turn them on.
            // May occur when the device is restricted under parental control.
        alert = [[UIAlertView alloc]initWithTitle:@""
                                          message:@"The functions of this app are limited because the Background app Refresh is disable."
                                         delegate:nil
                                cancelButtonTitle:@"Ok"
                                otherButtonTitles:nil, nil];
        [alert show];

    } else
    {

        // Background service is enabled, you can start the background supported location updates process
    }

After checking if the background services are available, we then need implement the following behaviors:

The Core Location Manager and it’s delegate, to receive location updates.
The explicitly starting and ending of the background tasks inc. stopping and refreshing the processes of the app.
Link the starting and ending of the background tasks to the Location Manager updates.
Object to save the returned values in order to use them later.

In our example we have three objects other than the Appdelegate and the ViewController:

BackgroundTaskManager: is a singleton and implements the starting and ending behavior of the background taks, hence the name BackgroundTaskManager.
LocationTracker: is a singleton as well, implements the Core Location Manager delegate, takes on itself to get notified when the app enters the background and links between the background tasks and the location updates.
LocationShareModel: is a singleton and has four global variables: two timers, backgroundTaskManager object and array of the locations; the first timer is a 60 seconds timer and is practically responsible of refreshing the app every 60 seconds and restarting the process so the app won’t terminate, the second timer is a 10 seconds timer, which is there for battery life reasons. consistently updating the devices location incl. when the app goes in the background can be a process that consumes a lot of battery over time. These two timers in play allow the app to receive location updates every 60 seconds, for 10 seconds in order to keep the battery consume as low as possible. The BackgroundTaskManager object is then used by the LocationTracker and the location-Array, to save the returned locations.

BackgroundTaskManager

The BackgroundTaskManager object has two global variables (masterTaskId and bgTaskIdList) that keep track of the background tasks, and four methods that start and end the background tasks.

The beginNewBackgroundTask method

-(UIBackgroundTaskIdentifier)beginNewBackgroundTask
{

// Once called, the beginNewBackgroundTask will start a new background task, if the app is indeed in the
// background, and will then explicitly end all the other tasks to prevent the app from being killed by the system

  UIApplication* application = [UIApplication sharedApplication];

    UIBackgroundTaskIdentifier bgTaskId = UIBackgroundTaskInvalid;
    if([application respondsToSelector:@selector(beginBackgroundTaskWithExpirationHandler:)])
        {

                bgTaskId = [application beginBackgroundTaskWithExpirationHandler:^{

                        NSLog(@"background task %lu expired", (unsigned long)bgTaskId);

                }];

                if ( self.masterTaskId == UIBackgroundTaskInvalid )
                {
                        self.masterTaskId = bgTaskId;
                        NSLog(@"started master task %lu", (unsigned long)self.masterTaskId);
                }
                else
                {
                       //add this id to our list
                        NSLog(@"started background task %lu", (unsigned long)bgTaskId);
                        [self.bgTaskIdList addObject:@(bgTaskId)];
                        // the endBackgroundTasks is simply a convenience method that ends all of the
                        // background tasks excl. the masterTask.
                        [self endBackgroundTasks];
                }
        }

    return bgTaskId;
}

The drainBGTaskList Method

// has a BOOL parameter, indicating if all background tasks should be stopped
// This method is called only through the two convenience methods
// endBackgroundTasks that passes NO as a parameter
// and endAllBackgroundTasks that passes YES as a parameter
-(void)drainBGTaskList:(BOOL)all
{
    //mark end of each of our background task
    UIApplication* application = [UIApplication sharedApplication];

    if([application respondsToSelector:@selector(endBackgroundTask:)])
    {
        NSUInteger count=self.bgTaskIdList.count;

        // when the "all" parameter is false, the integer value starts with one
        // the for then goes on ending all the previous tasks keeping only the one
        // that was just added
        for ( NSUInteger i=(all?0:1); i<count; i++ )             {     UIBackgroundTaskIdentifier bgTaskId = [[self.bgTaskIdList objectAtIndex:0] integerValue];     NSLog(@"ending background task with id -%lu", (unsigned long)bgTaskId);     [application endBackgroundTask:bgTaskId];     [self.bgTaskIdList removeObjectAtIndex:0];             }                      if ( self.bgTaskIdList.count > 0 )
            {
        NSLog(@"kept background task id %@", [self.bgTaskIdList objectAtIndex:0]);
            }

            if ( all )
            {
    // case "all" was true, all the tasks must be terminated, including the masterTask
    NSLog(@"no more background tasks running");
    [application endBackgroundTask:self.masterTaskId];
    self.masterTaskId = UIBackgroundTaskInvalid;
            } else
            {
     NSLog(@"kept master background task id %lu", (unsigned long)self.masterTaskId);
            }
    }
}

LocationTracker

After implementing the BackgroundTaskManager, comes the interesting part, here we will now have to implement the order of events that will keep refreshing the app and receiving location updates. Naturally we have the SharedModel as a global variable in order to have access to our BackgroundTaskManager object and the location array. Other than that we have the Core Location Manager delegate methods and five other important methods to start, stop, restart and terminate the process as well as a method that will be triggered through a local NSNotification when the app goes into the background.


- (void)startLocationTracking
{
        // Check for the location services authorizations and act accordingly

        // inc. the new iOS 8 way
        if(IS_OS_8_OR_LATER)
        {
          [locationManager requestAlwaysAuthorization];
        }
        [locationManager startUpdatingLocation];
}

// Stop the locationManager and the process completely
- (void)stopLocationTracking
{

    // Invalidate the timer and set it to nil
    //  so the process won’t repeat itself

        if (self.shareModel.timer)
        {
            [self.shareModel.timer invalidate];
            self.shareModel.timer = nil;
        }

        CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
        [locationManager stopUpdatingLocation];
}

//Restart the locationManager
- (void) restartLocationUpdates
{
    // Invalidate the timer and set it to nil
    // because we are restarting the process
    if (self.shareModel.timer)
    {
        [self.shareModel.timer invalidate];
        self.shareModel.timer = nil;
    }

    CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
    locationManager.delegate = self;
    // any further initialization that you see fit

    // check for iOS 8
    if(IS_OS_8_OR_LATER)
    {
        [locationManager requestAlwaysAuthorization];
    }
    [locationManager startUpdatingLocation];
}

//Stop the locationManager
-(void)stopLocationDelayBy10Seconds
{
    // This method is called by the 10 seconds timer -  "delay10Seconds"
    // in order to conserve battery life
    // The location updates will then be stopped
    // and restarted after 60 seconds by the 60 seconds timer - "timer"
    CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
    [locationManager stopUpdatingLocation];
}

// This Method will be called as soon as the app goes into the background
// (Which is done through the "[NSNotificationCenter defaultCenter] addObserver" method with the key
// "UIApplicationDidEnterBackgroundNotification
//" in the "name" parameter, should be implemented in the init method).
-(void)applicationEnterBackground
{
        CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
        locationManager.delegate = self;
        // Any other initializations you see fit

        // check for iOS 8
        if(IS_OS_8_OR_LATER)
        {
            [locationManager requestAlwaysAuthorization];
        }
        [locationManager startUpdatingLocation];

        //Use the BackgroundTaskManager to manage all the background Task
        self.shareModel.bgTask = [BackgroundTaskManager sharedBackgroundTaskManager];
        // Begin a new background task.
        [self.shareModel.bgTask beginNewBackgroundTask];
}

After going over these five methods, we still need one last piece in our puzzle. The Core Location Manager delegate method, didUpdateLocations:

-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations
{

    // filter the given locations as you see fit
    // and add them to the location array list
    for (int i = 0 ; i    {
        //…… filtering logic
        [self.shareModel.myLocationArray addObject:];
    }

    //If the timer still valid, it means the 60 seconds are not yet over, and any other
    // process shouldn’t be started, so return the method here (Will not run the code below)
    if (self.shareModel.timer)
    {
        return;
    }

    // start a new background task case app is in background
    self.shareModel.bgTask = [BackgroundTaskManager sharedBackgroundTaskManager];
    [self.shareModel.bgTask beginNewBackgroundTask];

    //Restart the locationMaanger after 1 minute
    self.shareModel.timer = [NSTimer scheduledTimerWithTimeInterval:60 target:self
                                                           selector:@selector(restartLocationUpdates)
                                                           userInfo:nil
                                                            repeats:NO];

    //Will only stop the locationManager after 10 seconds, so that we can get some accurate locations
    //The location manager will only operate for 10 seconds to save battery
    if (self.shareModel.delay10Seconds)
    {
        [self.shareModel.delay10Seconds invalidate];
        self.shareModel.delay10Seconds = nil;
    }

    self.shareModel.delay10Seconds = [NSTimer scheduledTimerWithTimeInterval:10 target:self
                                                                    selector:@selector(stopLocationDelayBy10Seconds)
                                                                    userInfo:nil
                                                                     repeats:NO];

}

And that is everything you need to know to make you receive location updates in the background without being terminated or killed by the iOS system. Again, you can find the source code link at the beginning of this part of the article.

Will this solution be approved by the Apple code review?

The answer to this question is probably yes, the provider of this example claims he has two live apps in the App Store using the given solution, he says so in one of the comments in his article where he shares this solution, here is the link. In the comment he also added a link to his portfolio.

For which use cases does it make sense to use this solution?

The backgroundTaskManager is there to keep the app alive and to allow it to constantly receive location updates in the background, the timers are there to keep the app from consuming huge amounts of battery life making, the interplay between the timers and the backgroundTaskManager makes it optimal to receive very accurate location updates on a long term basis. This solution is useful in apps that rely heavily on location, like GPS tracking apps or location-based dating apps. In other words, if you need a to constantly receive locations with a high accuracy, this solution is good for you.

What about Apple’s significant-change location service?

Apple’s significant-change location service, which includes calling the method: startMonitoringSignificantLocationChanges, has a low accuracy. The startMonitoringSignificantLocationChanges method initiates the delivery of location events asynchronously, returning them to the locationManager:didUpdateLocations: delegate method. After receiving a location fix, this method will update events only when a significant change in the user’s location is detected and will not answer to the distanceFilter property. So although this service relaunches your app, it will unfortunately do so in cases where for example the device becomes associated with a different cell tower. More information about the matter is here and here to be found. This will consume less power than the solution from this article, but it will also be much less accurate and you can’t really rely on a certain time or distance interval for location updates. So think about your use case before you decide for a certain solution.

Conclusions

Basically the main change in iOS is that Apple now gives the location authorization process to the developer and lets him decide the kind of access that he needs (foreground or background), in other word, if in the past the only thing we needed to get location updates was to call the method startUpdatingLocation and hope that the user gives the permission, now we need to take care of the authorization process. Which on the up side gives us more room to optimize the app for our exact needs, but on the down side makes it more complicated to work on the edge cases. Making the app working in the background is definitely the more complicated part of this article, but after going through the code two or three times everything becomes easier, hopefully for you as well! With this code being provable by Apple, the only thing left for you to do is to integrate it your app and to pass it to your exact needs, which shouldn’t be a problem after fully understanding the order of events, that is of course if that is the solution you were looking for. For other use cases, that do not need such a high accuracy background fetching solution, you can always still use Apple’s significant-change location service.

Where to now?

For a more detailed explanation about the location fetching changes in iOS 8, you should definitely check out the following article. And of course a link to the WWDC, what’s new in Core location. Are you interested in having your own location-based app for iOS (or Android) developed? Then just contact us.

Till next time,
Happy coding :)

VN:F [1.9.22_1171]
Rating: 4.6/5 (12 votes cast)
Core Location Manager in iOS 8 and fetching location in the background, 4.6 out of 5 based on 12 ratings

One Response so far.

Leave a Reply





Share:  
Mobile App Entwicklung und App Programmierung in Deutschland und Österreich. © App Agentur creative workline GmbH 2016
  App Entwicklung Berlin App Entwicklung Köln App Entwicklung München App Entwicklung Frankfurt App Entwicklung Hamburg App Entwicklung Hannover App Entwicklung Bremen App Entwicklung Düsseldorf App Entwicklung Deutschland App Entwicklung Wien App Entwicklung Österreich In Berlin App erstellen lassen
  App Programmierung Berlin App Programmierung Köln App Programmierung München App Programmierung Hamburg App Programmierung Hannover App Programmierung Bremen App Programmierung Deutschland App Programmierung Wien App Programmierung Österreich