Xcode 6.3.1 ARC Enabled, for iOS 8.3
I need help understanding a strange behavior that I encounter while trying to maintain a singleton shared timer in my application after it enters background.
Previously I did not care for this NSTimer as it was updated with user location in the background using background location services.
However, I would like to address the case where the user rejects location updates while in background or location updates all together. The location and the timer go hand in hand for my app.
I worked through quite a bit of information in the following threads and website posts...
Apple's - Background Execution
http://www.devfright.com/ios-7-background-app-refresh-tutorial/
http://www.infragistics.com/community/blogs/stevez/archive/2013/01/24/ios-tips-and-tricks-working-in-the-background.aspx
How do I make my App run an NSTimer in the background?
How do I create a NSTimer on a background thread?
Scheduled NSTimer when app is in background?
http://www.raywenderlich.com/92428/background-modes-ios-swift-tutorial
iPhone - Backgrounding to poll for events
Out of this information, I was able to derive (2) methods by which to keep the timer running for ~8 hours without iOS terminating the app after the allotted 180 sec. (3 min.) of execution time as promised by Apple documentation. Here is the code:
AppDelegate.h (unchanged for Method #1 or #2)
#import <UIKit/UIKit.h>
@interface MFAppDelegate : UIResponder <UIApplicationDelegate>
{
UIBackgroundTaskIdentifier bgTask;
NSInteger backgroundTimerCurrentValue;
}
@property (retain, nonatomic) NSTimer *someTimer;
@end
AppDelegate.m (Method #1)
<...
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
NSLog(@"applicationDidEnterBackground");
backgroundTimerCurrentValue = 0;
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
self.someTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop] addTimer:self.someTimer forMode:NSRunLoopCommonModes];
}
- (void)timerMethod
{
NSTimeInterval backgroundTimeRemaining = [[UIApplication sharedApplication] backgroundTimeRemaining];
if (backgroundTimeRemaining == DBL_MAX)
{
NSLog(@"Background Time Remaining = Undetermined");
}
else
{
NSLog(@"Background Time Remaining = %0.2f sec.", backgroundTimeRemaining);
}
if (!someBackgroundTaskStopCondition)
{
[self endBackgroundTask];
}
else
{
nil;
}
}
...>
AppDelegate.m (Method #2)
<...
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
NSLog(@"applicationDidEnterBackground");
backgroundTimerCurrentValue = 0;
bgTask = UIBackgroundTaskInvalid;
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
}];
self.someTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:true];
}
- (void)timerMethod
{
NSTimeInterval backgroundTimeRemaining = [[UIApplication sharedApplication] backgroundTimeRemaining];
if (backgroundTimeRemaining == DBL_MAX)
{
NSLog(@"Background Time Remaining = Undetermined");
}
else
{
NSLog(@"Background Time Remaining = %0.2f sec.", backgroundTimeRemaining);
}
if (backgroundTimerCurrentValue >= 500)
{
backgroundTimerCurrentValue = 0;
[self.someTimer invalidate];
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
}
else
{
NSLog(@"backgroundTimerCurrentValue: %ld", backgroundTimerCurrentValue);
backgroundTimerCurrentValue++;
}
}
...>
I have simulated the (2) methods overnight for ~8 hr. (28,800 sec.) by adjusting the if statement of (backgroundTimerCurrentValue >= 500). But to recreate the results for this question post, I used 500 sec. which is clearly more than 180 sec. These are the results from both methods:
...after 500 iterations of the self.someTimer, i.e. 500 sec.
Now, this was great, but after doing testing with iPhone 5s disconnected from Xcode, etc. Something strange happens... I made a single view application to check the value of the self.someTimer in a label.
Up to 180 sec. the label updates with the correct timer value. After 180 sec. and sometimes sooner, the application appears to remain selectable from slide of open apps, however when I tap on the screen to bring the application into foreground the application quickly restarts.
This behavior is what was promised by Apple, i.e. after 180 sec. and not having a valid endBackgroundTask:method. The method part is what I think is invalid being of type UIBackgroundTaskIdentifier as in method #2 and nil for method #1.
So my questions are as follows:
1) What is different from when the iPhone is plugged into my Mac running Xcode from when it is disconnected, does the system allow certain conditions to occur such as this seemingly endless background execution?
2) I can always get tricky with location services to start/stop them so that the timer value continues to update from some accumulation of time expired since last location update, but has anyone developed this code to actually run on a disconnected iPhone?
3) And if so in question 2, then is something like this legitimate and will be accepted when I submit my app for App Store?
Thanks in advance for your help. Cheers!