iPhone - Backgrounding to poll for events

IphoneBackgroundLocationMultitasking

Iphone Problem Overview


For quite a while I'd been looking into a way in my iPhone app to poll every X minutes to check the data counters. After much reading of the Background Execution documentation and a few trial apps I'd dismissed this as impossible without abusing the background APIs.

Last week I found this application which does exactly that. http://itunes.apple.com/us/app/dataman-real-time-data-usage/id393282873?mt=8

It runs in the background and keeps track of the count of Cellular/WiFi data you've used. I suspect that the developer is registering his app as tracking location changes but the location services icon isn't visible while the app is running, which I thought was a requirement.

Does anyone have any clues as to how this can be accomplished?

Iphone Solutions


Solution 1 - Iphone

I have seen this behavior, too. After trying a lot I discovered two things, which could help. But I am still uncertain how this may influence the reviewing process.

If you use one of the backgrounding features, the app will be launched by iOS in background again once it was quit (by the system). This we will abuse later.

In my case I used VoIP backgrounding enabled in my plist. All the code here is done in your AppDelegate:

// if the iOS device allows background execution,
// this Handler will be called
- (void)backgroundHandler {

	NSLog(@"### -->VOIP backgrounding callback");
	// try to do sth. According to Apple we have ONLY 30 seconds to perform this Task!
	// Else the Application will be terminated!
	UIApplication* app = [UIApplication sharedApplication];
    NSArray*    oldNotifications = [app scheduledLocalNotifications];

     // Clear out the old notification before scheduling a new one.
    if ([oldNotifications count] > 0) [app cancelAllLocalNotifications];

    // Create a new notification
    UILocalNotification* alarm = [[[UILocalNotification alloc] init] autorelease];
    if (alarm)
    {
        alarm.fireDate = [NSDate date];
        alarm.timeZone = [NSTimeZone defaultTimeZone];
        alarm.repeatInterval = 0;
        alarm.soundName = @"alarmsound.caf";
        alarm.alertBody = @"Don't Panic! This is just a Push-Notification Test.";
	
        [app scheduleLocalNotification:alarm];
    }
}

and the registration is done in

- (void)applicationDidEnterBackground:(UIApplication *)application {

    // This is where you can do your X Minutes, if >= 10Minutes is okay.
    BOOL backgroundAccepted = [[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{ [self backgroundHandler]; }];
	if (backgroundAccepted)
	{
		NSLog(@"VOIP backgrounding accepted");
	}
}

Now the magic happens: I don't even use VoIP-Sockets. But this 10 Minutes callback provides a nice side effect: After 10 Minutes (sometimes earlier) I discovered that my timers and previous running treads are being executed for a short while. You can see this, if you place some NSLog(..) into your code. This means, that this short "wakeup" executes the code for a while. According to Apple we have 30 seconds execution time left. I assume, that background code like threads are being executed for nearly 30 seconds. This is useful code, if you must "sometimes" check something.

The doc says that all background tasks (VoIP, audio, location updates) will be automatically restarted in background if the app was terminated. VoIP apps will be started in background automatically after bootup!

With abusing this behavior, you can make your app be looking like running "forever". Register for one background process (i.e. VoIP). This will cause your app to be restarted after termination.

Now write some "Task has to be finished" code. According to Apple you have some time (5 seconds?) left to finish tasks. I discovered, that this must be CPU time. So that means: if you do nothing, your app is still being executed! Apple suggest to call an expirationhandler, if you are finished with your work. In the code below you can see, that i have a comment at the expirationHandler. This will cause your app running as long as the system allows your app to be running. All timers and threads stay running until iOS terminates your app.

- (void)applicationDidEnterBackground:(UIApplication *)application {

	UIApplication*    app = [UIApplication sharedApplication];

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


    // Start the long-running task and return immediately.
	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
	
	// you can do sth. here, or simply do nothing!
    // All your background treads and timers are still being executed
    while (background) 
       [self doSomething];
       // This is where you can do your "X minutes" in seconds (here 10)
       sleep(10);
    }
	
    // And never call the expirationHandler, so your App runs
    // until the system terminates our process
    //[app endBackgroundTask:bgTask];
    //bgTask = UIBackgroundTaskInvalid;
	 
    });	
}

Be very spare with CPU-Time here, and your app runs longer! But one thing is for sure: your app will be terminated after a while. But because you registered your app as VoIP or one of the others, the system restarts the app in background, which will restart your background process ;-) With this PingPong I can do a lot of backgrounding. but remember be very spare with CPU time. And save all data, to restore your views - your app will be terminated some time later. To make it appear still running, you must jump back into your last "state" after wakeup.

I don't know if this is the approach of the apps you mentioned before, but it works for me.

Hope I could help

Update:

After measuring the time of the BG task, there was a surprise. The BG Task is limited to 600 seconds. This is the exact minimum time of the VoIP minimumtime (setKeepAliveTimeout:600).

So THIS code leads into "infinite" execution in background:

Header:

UIBackgroundTaskIdentifier bgTask; 

Code:

// if the iOS device allows background execution,
// this Handler will be called
- (void)backgroundHandler {

	NSLog(@"### -->VOIP backgrounding callback");

	UIApplication*    app = [UIApplication sharedApplication];

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

    // Start the long-running task 
	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
	
	while (1) {
		NSLog(@"BGTime left: %f", [UIApplication sharedApplication].backgroundTimeRemaining);
           [self doSomething];
		sleep(1);
	}	
});		

- (void)applicationDidEnterBackground:(UIApplication *)application {

    BOOL backgroundAccepted = [[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{ [self backgroundHandler]; }];
	if (backgroundAccepted)
	{
		NSLog(@"VOIP backgrounding accepted");
	}

	UIApplication*    app = [UIApplication sharedApplication];

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


    // Start the long-running task
	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
	
        while (1) {
            NSLog(@"BGTime left: %f", [UIApplication sharedApplication].backgroundTimeRemaining);
           [self doSomething];
           sleep(1);
        }	 
    });	
}

After your app has timed out, the VoIP expirationHandler will be called, where you simply restart a long running task. This task will be terminated after 600 seconds. But there will be again a call to the expiration handler, which starts another long running task, etc. Now you only have to check weather the App is getting back to foreground. Then close the bgTask, and you're done. Maybe one can do sth. like this inside the expirationHandler from the long running task. Just try it out. Use your Console, to see what happens... Have Fun!

Update 2:

Sometimes simplifying things helps. My new approach is this one:

- (void)applicationDidEnterBackground:(UIApplication *)application {

	UIApplication*    app = [UIApplication sharedApplication];

    // it's better to move "dispatch_block_t expirationHandler"
    // into your headerfile and initialize the code somewhere else
    // i.e. 
    // - (void)applicationDidFinishLaunching:(UIApplication *)application {
//
// expirationHandler = ^{ ... } }
    // because your app may crash if you initialize expirationHandler twice.
	dispatch_block_t expirationHandler;
	expirationHandler = ^{
	
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
	
	
		bgTask = [app beginBackgroundTaskWithExpirationHandler:expirationHandler];
    };

    bgTask = [app beginBackgroundTaskWithExpirationHandler:expirationHandler];


    // Start the long-running task and return immediately.
	dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
	
		// inform others to stop tasks, if you like
		[[NSNotificationCenter defaultCenter] postNotificationName:@"MyApplicationEntersBackground" object:self];
	
		// do your background work here		
    });	
}

This is working without the VoIP hack. According to the documentation, the expiration handler (in this case my 'expirationHandler' block) will be executed if execution time is over. By defining the block into a block variable, one can recursively start the long running task again within the expiration handler. This leads into endless execution, too.

Be aware to terminate the task, if your application enters foreground again. And terminate the task if you don't need it anymore.

For my own experience I measured something. Using the location callbacks with having the GPS radio on is sucking my battery down very quickly. Using the approach which I posted in Update 2 is taking nearly no energy. According to the "userexperience" this is a better approach. Maybe other Apps work like this, hiding its behavior behind GPS functionality ...

Solution 2 - Iphone

What Works & What Doesn't

It's not entirely clear which of these answers work & I've wasted a lot of time trying them all. So here's my experience with each strategy:

  1. VOIP hack - works, but will get you rejected if you're not a VOIP app
  2. Recursive beginBackgroundTask... - does not work. It will quit after 10 minutes. Even if you try the fixes in the comments (at least the comments up to Nov 30, 2012).
  3. Silent Audio - works, but people have been rejected for this
  4. Local/Push Notifications - require user interaction before your app will be woken up
  5. Using Background Location - works. Here are the details:

Basically you use the "location" background mode to keep your app running in the background. It does work, even if the user does not allow location updates. Even if the user presses the home button and launches another app, your app will still be running. It's also a battery drainer & may be a stretch in the approval process if your app has nothing to do with location, but as far as I know it's the only solution that has a good chance of being approved.

Here's how it works:

In your plist set:

  • Application does not run in background: NO
  • Required background modes: location

Then reference the CoreLocation framework (in Build Phases) and add this code somewhere in your app (before it goes into the background):

#import <CoreLocation/CoreLocation.h>

CLLocationManager* locationManager = [[CLLocationManager alloc] init];
[locationManager startUpdatingLocation];

Note: startMonitoringSignificantLocationChanges will not work.

It's also worth mentioning that if your app crashes, then iOS will not bring it back to life. The VOIP hack is the only one that can bring it back.

Solution 3 - Iphone

There is another technique to stay forever in the background - starting/stopping location manager in your background task, will reset the background timer when didUpdateToLocation: is called.

I don't know why it works, but I think didUpdateToLocation is also called as a task and thereby resets the timer.

Based on testing, I believe this is what DataMan Pro is using.

See this post https://stackoverflow.com/a/6465280 where I got trick from.

Here are some results from our app:

2012-02-06 15:21:01.520 **[1166:4027] BGTime left: 598.614497 
2012-02-06 15:21:02.897 **[1166:4027] BGTime left: 597.237567 
2012-02-06 15:21:04.106 **[1166:4027] BGTime left: 596.028215 
2012-02-06 15:21:05.306 **[1166:4027] BGTime left: 594.828474 
2012-02-06 15:21:06.515 **[1166:4027] BGTime left: 593.619191
2012-02-06 15:21:07.739 **[1166:4027] BGTime left: 592.395392 
2012-02-06 15:21:08.941 **[1166:4027] BGTime left: 591.193865 
2012-02-06 15:21:10.134 **[1166:4027] BGTime left: 590.001071
2012-02-06 15:21:11.339 **[1166:4027] BGTime left: 588.795573
2012-02-06 15:21:11.351 **[1166:707] startUpdatingLocation
2012-02-06 15:21:11.543 **[1166:707] didUpdateToLocation
2012-02-06 15:21:11.623 **[1166:707] stopUpdatingLocation
2012-02-06 15:21:13.050 **[1166:4027] BGTime left: 599.701993
2012-02-06 15:21:14.286 **[1166:4027] BGTime left: 598.465553

Solution 4 - Iphone

If it's not GPS I think the only other way of doing it is the background music function, i.e., playing 4"33" all the time it's enabled. Both sound like a bit of an abuse of the background processing APIs and so potentially subject to the whims of the review process.

Solution 5 - Iphone

I tried the Update 2 but it just doesn't work. When the expiration handler is called, it ends the background task. Then starting a new background task just forces an immediate call to the expiration handler again (the timer is not reset and is still expired). Thus I got 43 starts/stops of background tasks before the app was suspended.

Solution 6 - Iphone

in my tests on iOS5 i found it helps to have CoreLocation's monitoring started, via startMonitoringForLocationChangeEvents(not SignificantLocationChange), accuracy does not matter and it even this way on iPod if I do that - backgroundTimeRemaining is never goes down.

Solution 7 - Iphone

This is a rather old question, but the correct way to do this now is via Background App Refresh, which is fully supported by the OS and no hacks are needed.

Note that you are still at the mercy of the OS as to when your background refresh will actually occur. You can set a minimum poll time, but there is no guarantee it will be called at that frequency as the OS will apply its own logic to optimize battery life and radio use.

The first thing you need to do is configure your project to support background refresh in the Capabilities tab. This will add the necessary keys to your Info.plist:

enter image description here

Then you need to add some stuff to your AppDelegate, which needs to implement both URLSessionDelegate and URLSessionDownloadDelegate:

private var _backgroundCompletionHandler: ((UIBackgroundFetchResult) -> Void)?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
	...

	application.setMinimumBackgroundFetchInterval(5 * 60) // set preferred minimum background poll time (no guarantee of this interval)

	...
}

func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
	_backgroundCompletionHandler = completionHandler
	
	let sessionConfig = URLSessionConfiguration.background(withIdentifier: "com.yourapp.backgroundfetch")
	sessionConfig.sessionSendsLaunchEvents = true
	sessionConfig.isDiscretionary = true
	
	let session = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: OperationQueue.main)
	let task = session.downloadTask(with: url)
	task.resume()
}

func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
	NSLog("URLSession error: \(error.localizedDescription)")
	_backgroundCompletionHandler?(.failed)
	_backgroundCompletionHandler = nil
}

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
	NSLog("didFinishDownloading \(downloadTask.countOfBytesReceived) bytes to \(location)")
	var result = UIBackgroundFetchResult.noData
	
	do {
		let data = try Data(contentsOf: location)
		let json = try JSONSerialization.jsonObject(with: data, options: .mutableContainers)
		
		// process the fetched data and determine if there are new changes
		if changes {
			result = .newData

			// handle the changes here
		}
	} catch {
		result = .failed
		print("\(error.localizedDescription)")
	}
	
	_backgroundCompletionHandler?(result)
	_backgroundCompletionHandler = nil
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNeilInglisView Question on Stackoverflow
Solution 1 - IphoneJackPearseView Answer on Stackoverflow
Solution 2 - IphonebendytreeView Answer on Stackoverflow
Solution 3 - IphoneKashif ShaikhView Answer on Stackoverflow
Solution 4 - IphoneStephen DarlingtonView Answer on Stackoverflow
Solution 5 - IphoneiOSTesterView Answer on Stackoverflow
Solution 6 - IphonedkzmView Answer on Stackoverflow
Solution 7 - Iphonedevios1View Answer on Stackoverflow