Carielle
Carielle

Reputation: 11

iOS 8 Background Location Update triggered by iBeacon

I am trying to make an app that can be trigged by an iBeacon to wake up (from being killed/suspended/terminated) to record second-by-second GPS information. The GPS recording should then stop when the phone gets out of range of the beacon. I have successfully gotten my app to recognize the didEnterRegion and didExitRegion methods when it comes in and out of range of the iBeacon. In the didEnterRegion method I want to basically say something like [locationManager startUpdatingLocation] so that I can start tracking the user's location. However, when I try to add this line of code, the location updates stop after about 10 seconds.

Later I found an article about background location updates that came with this Github project. I added the BackgroundTaskManager, LocationShareModel, and LocationTracker files to my project. Basically, the idea behind this solution is to continually restart the location manager so it doesn't have the chance for the background task to expire and stop sending updates. However, even with this solution, I only get location updates for a little over 3 minutes.

I have the "Location Updates" and "Use Bluetooth LE accessories" background modes enables. The "Background Fetch" (Background App Refresh) is not enabled, in accordance with this quote from Apple: "In iOS 8 and later, disabling the Background App Refresh setting for the current app or for all apps does not prevent the delivery of location events in the background." My app requests "Always" authorization for location updates.

I cannot figure out how to solve this issue, despite reviewing seemingly endless StackOverflow articles and tutorials. I am testing it on an iPhone 5S running iOS 8.3.0. Any insight would be appreciated. See code excerpts below.

In AppDelegate.m :

- (void)locationManager:(CLLocationManager *)manager didEnterRegion:(CLRegion *)region {
    if ([region isKindOfClass:[CLBeaconRegion class]]) {
        UILocalNotification *notification = [[UILocalNotification alloc] init];
        notification.alertBody = @"Start recording trip";
        notification.soundName = @"Default";
        [[UIApplication sharedApplication] presentLocalNotificationNow:notification];

        self.recording = YES;
        [self startAutoTrip];
    }
}

- (void) startAutoTrip {
    self.locationTracker = [[LocationTracker alloc]init];
    [self.locationTracker startLocationTracking];
}

- (void)locationManager:(CLLocationManager *)manager didExitRegion:(CLRegion *)region {
    if ([region isKindOfClass:[CLBeaconRegion class]]) {
        UILocalNotification *notification = [[UILocalNotification alloc] init];
        notification.alertBody = @"Stop recording trip";
        notification.soundName = @"Default";
        [[UIApplication sharedApplication] presentLocalNotificationNow:notification];

        [self stopAutoTrip];
        self.recording = NO;
    }
}

- (void)stopAutoTrip {

    // stop recording the locations
    CLSLog(@"Trying to stop location updates");
    [self.locationTracker stopLocationTracking:self.managedObjectContext];
    CLSLog(@"Stop location updates");
}

In LocationTracker.m (from tutorial cited above, change 60 sec and 10 sec time intervals to 5 sec and 2 sec). Basically these are the startLocationTracking, didUpdateLocations, and stopLocationTracking methods.

- (void)startLocationTracking {
NSLog(@"startLocationTracking");

    if ([CLLocationManager locationServicesEnabled] == NO) {
        NSLog(@"locationServicesEnabled false");
        UIAlertView *servicesDisabledAlert = [[UIAlertView alloc] initWithTitle:@"Location Services Disabled" message:@"You currently have all location services for this device disabled" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [servicesDisabledAlert show];
    } else {
        CLAuthorizationStatus authorizationStatus= [CLLocationManager authorizationStatus];

        if(authorizationStatus == kCLAuthorizationStatusDenied || authorizationStatus == kCLAuthorizationStatusRestricted){
            NSLog(@"authorizationStatus failed");
        } else {
            NSLog(@"authorizationStatus authorized");
            CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
            locationManager.delegate = self;
            locationManager.desiredAccuracy = kCLLocationAccuracyBest;
            locationManager.distanceFilter = 10; //meters
            locationManager.activityType = CLActivityTypeAutomotiveNavigation;
            locationManager.pausesLocationUpdatesAutomatically = NO;

            if(IS_OS_8_OR_LATER) {
                [locationManager requestAlwaysAuthorization];
            }
            [locationManager startUpdatingLocation];
        }
    }
}

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

    NSLog(@"locationManager didUpdateLocations");

    for(int i=0;i<locations.count;i++){
        CLLocation * newLocation = [locations objectAtIndex:i];

        NSDate *eventDate = newLocation.timestamp;

        NSTimeInterval howRecent = [eventDate timeIntervalSinceNow];

        if (fabs(howRecent) < 10.0 && newLocation.horizontalAccuracy < 20 && locations.count > 0) {


            CLLocationCoordinate2D theLocation = newLocation.coordinate;
            CLLocationAccuracy theAccuracy = newLocation.horizontalAccuracy;

            self.myLastLocation = theLocation;
            self.myLastLocationAccuracy= theAccuracy;

            CLLocationCoordinate2D coords[2];
            coords[0] = ((CLLocation *)locations.lastObject).coordinate;
            coords[1] = newLocation.coordinate;

            [self.shareModel.myLocationArray addObject:newLocation];

        }
    }

    //If the timer still valid, return it (Will not run the code below)
    if (self.shareModel.timer) {
        return;
    }

    self.shareModel.bgTask = [BackgroundTaskManager sharedBackgroundTaskManager];
    [self.shareModel.bgTask beginNewBackgroundTask];

    //Restart the locationMaanger after 1 minute (5 sec)
    self.shareModel.timer = [NSTimer scheduledTimerWithTimeInterval:5 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
    // 2 sec
    if (self.shareModel.delay10Seconds) {
        [self.shareModel.delay10Seconds invalidate];
        self.shareModel.delay10Seconds = nil;
    }

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

}

- (void) restartLocationUpdates
{
    NSLog(@"restartLocationUpdates");

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

    CLLocationManager *locationManager = [LocationTracker sharedLocationManager];
    locationManager.delegate = self;
    locationManager.desiredAccuracy = kCLLocationAccuracyBest;
    locationManager.distanceFilter = 10; //meters
    locationManager.activityType = CLActivityTypeAutomotiveNavigation;
    locationManager.pausesLocationUpdatesAutomatically = NO;

    if(IS_OS_8_OR_LATER) {
        [locationManager requestAlwaysAuthorization];
    }
    [locationManager startUpdatingLocation];
}

- (void)stopLocationTracking:(NSManagedObjectContext *)managedObjectContext       {
    NSLog(@"stopLocationTracking");
    CLSLog(@"stopLocationTracking");

    CLSLog(@"set managedObjectContext %@", managedObjectContext);
    self.managedObjectContext = managedObjectContext;

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

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

    [self saveRun];
    [self sendRun];
}

Upvotes: 0

Views: 1030

Answers (3)

Carielle
Carielle

Reputation: 11

Thank you all for your responses. It is possible to wake your app up from being killed/suspended/terminated using iBeacons, contrary to what Øyvind Hauge said. And unfortunately, adding the background location mode to your plist does not enable indefinite location updates, as others suggested; I was only ever able to get 3 minutes of execution using that method.

I actually found the solution to my question in this StackOverflow article. The solution is to add just a few lines of code to your app delegate - you need to start another location manager that is monitoring for significant location updates. Here are the lines of code that I added to my didFinishLaunchingWithOptions method in my AppDelegate.m file after declaring anotherLocationManager as a property...

self.anotherLocationManager = [[CLLocationManager alloc] init];
self.anotherLocationManager.delegate = self;
[self.anotherLocationManager startMonitoringSignificantLocationChanges];

I never do anything else using this location manager, I just leave it perpetually running in the background, and for some reason this enables indefinite location updates from a regular call to [locationManager startUpdatingLocation]. I am no longer having the location updates mysteriously stop after 3 minutes. It seems very strange that this was the solution, but it was pretty simple to implement, and hopefully this will help others who are dealing with the same problem.

Upvotes: 1

user3334059
user3334059

Reputation: 507

So in iOS, location updates will work in background indefinitely ONLY if - 1. You have started location updates in foreground AND 2. You have added Background Location in your plist.

In your case, the OS is waking you up in background and as you've said correctly, you only get 10 seconds of execution time before the OS suspends your app. The workaround for this is basically starting a background task, as you have done to get additional 180 seconds of execution time (this number can change based on OS version).

To understand your issue in depth, you need to know that there are only certain events(like geofence/ibeacon/significant location update) which will wake your app in background, let us call them "wakeup" events. Once any of these event occurs, you have a maximum of 180 seconds of background execution time (using background task) after which your app WILL be suspended, unless any of these events is triggered again, after which you need to restart your background task. I'm not sure how your application works exactly, but if you can ensure that you keep getting these "wakeup" events from the OS for the duration for which you need location updates, you can pretty much keep your app awake in background.

Just to add, I've seen a lot of blog posts that claim that keeping a timer and restarting location updates periodically using that timer works, but I have never been able to use it successfully.

Upvotes: 0

davidgyoung
davidgyoung

Reputation: 64916

If you set the location background mode in your plist, you can range beacons and get GPS location updates indefinitely. The key to getting this to work is starting a background thread.

You can see an example of how to do this in this blog post about extending beacon ranging on the background. While the blog post mentions this is limited to 3 minutes, when you add the location background mode to your plist, that time limit goes away.

Understand that you may not get AppStore approval for using this background mode unless Apple appreciates your justification for doing so.

Upvotes: 0

Related Questions