Energy-saving background location + sending data to the server from the background

    Formulation of the problem

    In the application, you need to track the user's location when the application is running in the background (with relatively acceptable accuracy), and also when the application is active (with high accuracy).

    Decision

    The solution to the forehead is to use the data from the [CLLocationManagerInstance startUpdatingLocation] callbacks both in the background and when the application is active. The first and most critical drawback of this solution is its high power consumption (in a few hours, the iPhone battery can completely run out). The second - if the application is minimized and 'killed', we can’t get any updates to the user's position.

    To solve these two problems, as well as to make this solution separate and not related to the main application code, we will write our component that will use [CLLocationManagerInstance startUpdatingLocation] in the active application mode and [CLLocationManagerInstance startMonitoringSignificantLocationChanges]in the background. There will be two blocks in the component, which will be executed depending on the state of the application.

    User location

    Foreground

    For an active application, the solution is obvious - we need to create a CLLocationManager instance and install a delegate, and then process the received data in the callbacks. Create a wrapper object:

    #import 
    typedef void(^locationHandler)(CLLocation *location);
    @interface DLLocationTracker : NSObject 
    @property (nonatomic, strong) CLLocationManager *locationManager;
    @property (nonatomic, copy) locationHandler locationUpdatedInForeground;
    - (void)startUpdatingLocation;
    - (void)stopUpdatingLocation;
    @end
    


    The locationUpdatedInForeground block will be executed when the user’s location is updated. The object is created in the controller, then you need to call the startUpdatingLocation method to start the service.

    Background

    As mentioned above, there are two main ways to get coordinate updates in the background:
    • Set UIBackgroundModes = "location" in * .plist application, and use [locationManager startUpdatingLocation] - a very energy-consuming, but accurate way;
    • Use Significant Location Changes (> iOS 4.0) - energy-efficient, uses data from cellular networks. It is updated approximately once every 10-15 minutes, the error is up to 500 meters (determined experimentally). More details can be found here .

    We will use the second approach.
    Update the header of our component:

    #import 
    typedef void(^locationHandler)(CLLocation *location);
    @interface DLLocationTracker : NSObject 
    @property (nonatomic, strong) CLLocationManager *locationManager;
    @property (nonatomic, copy) locationHandler locationUpdatedInForeground;
    @property (nonatomic, copy) locationHandler locationUpdatedInBackground;
    - (void)startUpdatingLocation;
    - (void)stopUpdatingLocation;
    - (void)endBackgroundTask;
    @end
    


    The locationUpdatedInBackground block will be called when the application receives a coordinate update in the background.
    endBackgroundTask - a method that allows you to finish a task running in the background (we 'll cover later).

    Also in the * .plist application you need to add the item Required background modes = {App registers for location updates} .

    The Significant Location Changes mechanism allows you to receive location updates even if the application is not running. To do this, you need to slightly rewrite the standard method appDelegate applicationDidFinishLaunchingWithOptions :

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
     	if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) {
            self.locationTracker = [[DLLocationTracker alloc] init];
            [self.locationTracker setLocationUpdatedInBackground:^(CLLocation *location) {
    //тестовый блок, будет показывать local notification с координатами
                UILocalNotification *notification = [[UILocalNotification alloc] init];
                notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:15];
                notification.alertBody = [NSString stringWithFormat:@"New location: %@", location];
                [[UIApplication sharedApplication] scheduleLocalNotification:notification];
            }];
            [self.locationTracker startUpdatingLocation];
        }
         .....
    }
    


    UIApplicationLaunchOptionsLocationKey - a key that indicates that the application was launched in response to a received location change event.

    Component implementation

    When the component is initialized, a CLLocationManager instance is created and the object is set by its delegate, we also sign it for notifications about changes in the state of the application (active / background).

    - (id)init {
        if (self = [super init]) {
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
            [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground) name:    UIApplicationDidEnterBackgroundNotification object:nil];
            self.locationManager = [[CLLocationManager alloc] init];
            self.locationManager.delegate = self;
        }
        return self;
    }
    


    Next, call startUpdatingLocation:

    - (void)startUpdatingLocation {
        [self stopUpdatingLocation];
        [self isInBackground] ? [self.locationManager startMonitoringSignificantLocationChanges] : [self.locationManager startUpdatingLocation];
    }

    Depending on the state of the application, the desired service is activated.
    All the most interesting happens in the CLLocationManager callback:

    - (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
        //фильтруем апдейты на основании минимального времени обновления и минимально дистанции 
        if (oldLocation && ([newLocation.timestamp timeIntervalSinceDate:oldLocation.timestamp] < kMinUpdateTime ||
                            [newLocation distanceFromLocation:oldLocation] < kMinUpdateDistance)) {
            return;
        }
        if ([self isInBackground]) {
            if (self.locationUpdatedInBackground) {
                bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^{
                    [[UIApplication sharedApplication] endBackgroundTask:bgTask];
                }];
                self.locationUpdatedInBackground(newLocation);
                [self endBackgroundTask];
            }
        } else {
            //если приложение активно - выполняем этот блок
            if (self.locationUpdatedInForeground) {
                self.locationUpdatedInForeground(newLocation);
            }
        }
    }

    In order for our application to be able to do anything in the background, you need to call the beginBackgroundTaskWithExpirationHandler method and initialize the bgTask identifier (type UIBackgroundTaskIdentifier). Each call to this method must be balanced by calling endBackgroundTask :, which is what happens in [self endBackgroundTask] :

    - (void)endBackgroundTask {
        if (bgTask != UIBackgroundTaskInvalid) {
            [[UIApplication sharedApplication] endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
        }
    }

    The important point is that the locationUpdatedInBackground block is executed synchronously (we can afford it when the applications are in the background), this can cause problems if you minimize / expand the application during the block execution, namely, if the block does not execute within 10 seconds, the application will crash.

    Asynchronously sending data from the background

    To send silently asynchronously, change the code of our component:

        if ([self isInBackground]) {
            if (self.locationUpdatedInBackground) {
                bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler: ^{
                    [[UIApplication sharedApplication] endBackgroundTask:bgTask];
                }];
                self.locationUpdatedInBackground(newLocation);
                //[self endBackgroundTask]; - заканчивать таск будем по коллбекам нашей асинхронной операции в реализации блока
            }
    

    LocationUpdatedInBackground block :

        __weak DLLocationTracker *lc = self.locationTracker;
        [self.locationTracker setLocationUpdatedInBackground:^ (CLLocation *location) {
    //предположим, что у нас есть метод с completion и fail хендлерами для отправки местоположения
            [self sendLocationToServer:location completion:^{
                   [lc endBackGroundTask];
            } fail:^(NSError *fail) {
                   [lc endBackGroundTask];
            }];
        }];


    Conclusion

    A similar energy-efficient method is used in many applications. For example, the Radar feature in Forsquare. The code of the test application can be taken on github .

    Also popular now: