4

We're currently developing an iOS app that needs to check location in the background. At first, we tried to use significant location changes, but they aren't accurate enough/don't trigger often enough. We considered using region monitoring, but from what I've read online, that isn't always accurate either, and you also have the problem of a limited number of regions to monitor. (We may eventually try region monitoring.) At the moment, however, we're attempting to use the standard location updates to track the user location in the background, with a plan to have it at check at intervals of 5 minutes, or so.

The app is registered for location updates in the background (using 'App registers for location updates' for 'Required background modes'), and we start a background task which checks the location once, stops location updates, then uses NSThread sleepForTimeInterval: to (at the moment, while we're in development) pause the task for 10 seconds. It then checks the location once again, stops location updates, pauses for 10 seconds, etc.

This appears to work as expected... When the app goes into the background, we receive a log/notification with our location update every 10 seconds, and when the app is reopened, the logs/notifications stop. However, the problem is that when the app then goes into the background for a second time, it appears the original background task was never cancelled, and a new one is created, so there are now two tasks running, each checking location at 10 sec on intervals. If the app is opened/sent to the background multiple times, then a background task is started for each of them.

I thought about setting a flag to say "has the app been sent to the background at least once?", and only run the task if it's the first time it's sent to the background, but this seems to cause additional problems, and (as a relatively new iOS developer) I'm curious as to why the background tasks aren't being cancelled when the app enters the foreground.

The AppDelegate.h file contains...

@interface AppDelegate : UIResponder <UIApplicationDelegate, CLLocationManagerDelegate> {
    UIWindow *window;
    UINavigationController *navigationController;

    UIBackgroundTaskIdentifier bgTask;
    BOOL inBackground;
}

The AppDelegate.m file contains...

- (void)applicationDidEnterBackground:(UIApplication *)application {
    inBackground = YES;

    bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
        [[UIApplication sharedApplication] endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while (inBackground == YES) {
            NSLog(@"%@", @"Check location...");
            [locationManager startUpdatingLocation];

            [NSThread sleepForTimeInterval:10];
        }

        [[UIApplication sharedApplication] endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    });
}

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    inBackground = NO;

    [[UIApplication sharedApplication] endBackgroundTask:bgTask];
    bgTask = UIBackgroundTaskInvalid;
}

The location updates are working as expected, I just can't work out why the background tasks aren't being cancelled/ended when the app enters the foreground. I do wonder if it's anything to do with the NSThread sleepForTimeInterval:, but I'm not sure if it is, or how to fix it (if indeed, it is). Thanks, in advance, for any help!

Cœur
  • 37,241
  • 25
  • 195
  • 267
dvyio
  • 339
  • 1
  • 7
  • 20
  • You should UIBackgroundMode to your info.plist file and identify your application as a background location application. – J2theC Aug 23 '12 at 14:46
  • Thanks, J2theC, but we already have 'App registers for location updates' for 'Required background modes'. (I'll edit the question to make this clear.) The location updates occur as they should, it's just this never-ending background task that's the problem. – dvyio Aug 23 '12 at 14:50
  • 1
    I'm fairly certain that your instance variable `bgTask` is being reallocated when the app comes back into the foreground, so the value doesn't contain the identifier you're looking to kill. Consider saving this identifier in `NSUserDefaults` or something a little more permanent. – Hyperbole Aug 23 '12 at 14:53

2 Answers2

2

You don't manage location updates by sleeping and then requesting them. You manage location updates by setting "location" in UIBackgroundMode (as you do), and then implementing a CLLocationManagerDelegate. This has nothing to do with beginBackgroundTaskWithExpirationHandler:. That's for requesting additional time (up to about 10 minutes) to finish a given operation. You shouldn't be calling that at all just to get location updates.

Once you've registered as a location app in UIBackgroundMode, you will automatically get updates whenever the location changes within the accuracy you specified for your location manager. The system will do all the work for you.


What you're describing may actually hurt battery life because it frustrates the OS's ability to manage the multiple location sensors (of which the GPS is just one). Tell the OS what you need by setting the correct accuracy (if significant changes is too coarse), and let it do its job. Getting really accurate location from the GPS is expensive. You should do battery testing before assuming that it's cheaper to do every 5 minutes than to leave on. The best thing you can do to preserve power is to reduce the required accuracy. You might turn it down to a coarse level, and then when you come to the foreground move it to an accurate level. But keeping track of precisely where the user is every 5 minutes is going to be expensive. It's hard to fix that.

BTW, what you're really trying to do here is get to run "something" every 5 minutes. There is no mechanism for that in iOS. You can either ask for location services or not (and configure it in various ways). You can't ask for "I want to wake up every five minutes and ... do anything." After about 10 minutes you're going to be killed if you don't call endBackgroundTask:.

To your question of why the tasks aren't being cancelled, see How to use beginBackgroundTaskWithExpirationHandler for already running task in iOS. As I said, this "background tasks" is not the tool you want for this problem. It's completely unrelated.

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thanks, Rob. That was something we tried originally, but we don't need constant location updates. We only need the location every x minutes, while still trying to conserve the user's battery. If significant location change or region monitoring were more accurate, they would be perfect, but we need more accuracy than they currently provide. By checking the location every x minutes (with `CLLocationManagerDelegate`), then turning the location updates off immediately (until the next check), we can get an accurate location without constant updates. It's currently working as hoped, I think. – dvyio Aug 23 '12 at 20:22
  • I'll definitely try standard location updates with reduced accuracy, as you suggest. Ideally, we only need a user's location every 30 minutes, but obviously, that's not currently possible. It would be great if there was an API that worked like significant location change, but consistently gave an update after every 100 metres (or so) of movement. As a side note, it seems that if you're running a background task, then request a location update while in that background task, the allowed time remaining for that background task is reset. – dvyio Aug 23 '12 at 20:54
  • If you've set "location" then you shouldn't be killed in any case. You shouldn't need backgroundTask. If you only need 100m accuracy, then set kCLLocationAccuracyHundredMeters. If you want high accuracy, but don't want to be called very often, that's not going to help battery life. Calling you is very cheap. Keeping track of accurate location is expensive. If you need accuracy "sometimes" you can set kCLLocationAccuracyHundredMeters and go into the background. When you wake up, check the accuracy. Then set kCLLocationAccuracyBest. If you wake up again with better accuracy, use that. – Rob Napier Aug 23 '12 at 20:59
1

I'm fairly certain that your instance variable bgTask is being reallocated when the app comes back into the foreground, so the value doesn't contain the identifier you're looking to kill. Consider saving this identifier in NSUserDefaults or something a little more permanent and retrieving it later.

Hyperbole
  • 3,917
  • 4
  • 35
  • 54
  • Thanks! I commented out the `bgTask = UIBackgroundTaskInvalid` in `- (void)applicationWillEnterForeground:(UIApplication *)application`, and also added an if/else in `- (void)applicationDidEnterBackground:(UIApplication *)application`, to see if `bgTask` existed. It now only creates a new `bgTask` if it doesn't exist, and it now seems to work! Do you think this is a reasonable solution? It seems that `bgTask = UIBackgroundTaskInvalid` was causing a problem, but I thought that was meant to be there. – dvyio Aug 24 '12 at 14:40
  • Setting the value to `UIBackgroundTaskInvalid` is what you should be doing when you're done with the identifier. I don't understand what you're describing in your comment, could you update your answer with the code you've arrived at? – Hyperbole Aug 24 '12 at 14:57