As far i know, there are two distinct stages of iOS application lifecycle when it is hidden:
Background - when we are still able to proceed computation and run code;
Suspended - when any computation is freezed and we can't do anything till the user returns to the app.
So i'm a bit confused about a following example:
- (void)applicationDidEnterBackground:(UIApplication *)application {
(void)(^expirationHandler)() = [^{
[application endBackgroundTask:bgTask];
self.bgTask = UIBackgroundTaskInvalid;
} copy];
self.bgTask = [application beginBackgroundTaskWithName:#"MyTask" expirationHandler:expirationHandler];
[NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(count)
userInfo:nil
repeats:YES];
expirationHandler();
}
- (void)count {
int i = 0;
while (i < 10000000) {
i++;
}
NSLog(#"%d", i);
}
P.S. I've reproduced it from memory, let me know if something wrong, but question is not related to correctness of the snippet.
I tested it on iPhone 6+ with iOS 9.3 and the specified timer persists firing, furthermore count method executes code (you can check it by i value). Am i right that app must proceed to suspended state after the timer scheduled? If so, why is count able to execute code?
Thanks in advance.
As per #Paulw11 in comments,
Are you testing while running under the debugger in Xcode? If so, then you app doesn't have background time limits or get suspended
I haven't found any proofs, but through empirical enquiry it appears to be true.
Related
I'm working on an sdk that uses NSTimer to make a server call when it expires at x mins and the server call resets the timer back to x mins after doing some work. The problem I'm having is that my timer stops running when my app is in the background and resumes immediately when my app is back in the foreground. How do I get this to work?
//initialize self.myTimer somewhere in my code
//called once somewhere in my code-->[self resetTimer:self.myTimer expiry:30];
- (void)resetTimer:(NSTimer *)timer expiry:(float)seconds {
[timer invalidate];
NSTimer *newTimer = [NSTimer timerWithTimeInterval:seconds
target:self
selector:#selector(updateTimer:)
userInfo:nil
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:newTimer forMode:NSRunLoopCommonModes];
self.myTimer = newTimer;
}
- (void) updateTimer:(NSTimer *)timer {
[timer invalidate];
dispatch_queue_t queue = dispatch_queue_create("update", NULL);
dispatch_async(queue, ^{
[self serverCall];
});
}
-(void)serverCall{
//make server call and do some work
[self resetTimer:self.myTimer expiry:30];
}
The timer won't continue to fire when the app is in the background. If you're willing to let the OS set the pace of the server calls, you can accomplish this by consulting the section "Fetching Small Amounts of Content Opportunistically" in this background execution doc.
The gist is to set your app's UIBackgroundModes key == fetch in info.plist.
When iOS decides to grant your app some cycles, you can run a short task in:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[self serverCall]; // BUT! read on
}
But you'll need to refactor your serverCall to tell the caller when its done. Otherwise, you won't know when to invoke the completionHandler:.
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[self newServerCallWithCompletion:^(BOOL gotNewData) {
UIBackgroundFetchResult result = (gotNewData)? UIBackgroundFetchResultNewData : UIBackgroundFetchResultNoData;
completionHandler(result);
}];
}
See the docs for more options on UIBackgroundFetchResult.
With this, iOS will set the pace of requests, so you won't be able to make them more often than when you're app is given the chance. By persisting the time of the last request, you can make requests less often. Just check if the interval since the last request is <= to some desired max frequency. If it is, just call the completionHandler right away with "no data".
I'am working on an application in xcode-5, I need to upload some data in background mode, I need a set of code to be repeated till the device terminates the background process. I am able to do it once but I need to repeat again and again so that i can check the connectivity and perform the upload. I doing the same thing when the application comes in active mode.
Currently I'm using Timers with the following code :
- (void)applicationDidEnterBackground:(UIApplication *)application
{
[bgrdTimer_ActiveMode invalidate];
bgrdTimer_ActiveMode=nil;
self.backgroundTask=[application beginBackgroundTaskWithExpirationHandler:^{
self.backgroundTask=UIBackgroundTaskInvalid;
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0),^{
bgrdTimer_BackGroundMode=[NSTimer scheduledTimerWithTimeInterval:40.00 target:self selector:#selector(repeatUploadProcess_BackGroundMode) userInfo:nil repeats:YES];
});
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
[bgrdTimer_BackGroundMode invalidate];
bgrdTimer_BackGroundMode=nil;
bgrdTimer_ActiveMode=[NSTimer scheduledTimerWithTimeInterval:40.00 target:self selector:#selector(repeatUploadProcess_ActiveMode) userInfo:nil repeats:YES];
}
repeatUploadProcess_ActiveMode and repeatUploadProcess_BackGroundMode are the methods containing the set of code which is suppose to be repeated
the problem is when I invalidate the bgrdTimer_ActiveMode the other timer dosn't gets invoked.
Have you enabled the background fetch mode? Click on the project target, and under the capabilities tab select "background fetch".
Theoretically you should also be setting [application endBackgroundTask:bgTask]. It sounds like you know, but if it never ends, iOS will end up killing your app.
When my app is downloading big file and user switching to the other app, i'm running background task like this:
beginBackgroundTaskWithExpirationHandler:
and then if user opening "app switcher" by double click, screenshot of my app is completely random. Sometimes it's showing view controller that was not even open in the app.
ignoreSnapshotOnNextApplicationLaunch not helping, because it's not working at all.
Apple says: Avoid updating your windows and views here: documentation, but I'm not updating views.
I'm also running timer, to check how much background time is left, and this timer is the cause of my problems. If I'm not creating it, everything is working perfect, but I cannot save download state in Expiration handler - not enough time.
How can i avoid this weird behaviour?
-(void)appDidEnterBackground {
UIApplication *application = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier bgTask;
bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
if(backgroundTimer == nil || ![backgroundTimer isValid]) {
backgroundTimer = [[NSTimer alloc]
initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:0]
interval:1
target:self
selector:#selector(checkBackgroundTimeRemaining)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:backgroundTimer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
}
}
- (void)checkBackgroundTimeRemaining {
NSTimeInterval timeRemaining = [[UIApplication sharedApplication] backgroundTimeRemaining];
if(timeRemaining < 5) {
if(backgroundTimer != nil) {
[backgroundTimer invalidate];
backgroundTimer = nil;
}
[downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
[self saveResumeData:resumeData];
}];
}
}
Sometimes it's showing view controller that was not even open in the app.
This sounds really fishy and should never happen. Maybe you can add some code to show what you are doing?
ignoreSnapshotOnNextApplicationLaunch is irrelevant here since it's only used to determine what happens when the user taps on your icon again to open the app.
Did you maybe forget to call endBackgroundTask: when you've finished your background task?
I'm not sure what you intend with the timer? If it is to determine how much time is left for you to execute in the background, use UIApplication's backgroundTimeRemaining instead.
I am trying to get this to work correctly but it always seems to end early at 174 seconds (2.9 mins). I been following every tutorial possible online on how to use beginBackgroundTaskWithExpirationHandler and I don't see anything wrong with my code. I need this to end at 8.5 mins. The endBackgroundTask method doesn't even gets call before the expiration handler gets called. Is anything wrong with this?
- (void)applicationDidEnterBackground:(UIApplication *)application {
if([[UIDevice currentDevice] respondsToSelector:#selector(isMultitaskingSupported)])
{
NSLog(#"Multitasking Supported");
backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^ {
NSLog(#"Terminated");
//Clean up code. Tell the system that we are done.
[application endBackgroundTask: backgroundTask];
backgroundTask = UIBackgroundTaskInvalid;
}];
timer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:#selector(count:)
userInfo:nil
repeats:YES];
}
else
{
NSLog(#"Multitasking Not Supported");
}
}
-(void)turnOffDesktops:(NSTimer *)time {
//do smth
if(count < (60*8.5)){
NSLog(#"%d",count);
count++;
}else{
[application endBackgroundTask: backgroundTask];
backgroundTask = UIBackgroundTaskInvalid;
[timer invalidate];
timer = nil;
count = 0;
}
}
There has never been a commitment from Apple on how long you would be allowed to perform your background tasks. Historically (until iOS7), apps were given usually 10 minutes to run in the background. This is no longer the case! Watch the WWDC 2013 video on backgrounding. With the addition of the new download & upload API in NSURLSession (ability to schedule download and upload tasks on an external dedicated daemon), Apple has reduced the allowed background time significantly. They've done this because this API has always been meant for download and upload, not arbitrary tasks in the background.
You can determine the amount of time left in background by querying - [UIApplication backgroundTimeRemaining]. You can use this to schedule your code to start at the latest possible.
This question already has answers here:
backgroundTimeRemaining returns (35791394 mins)?
(2 answers)
Closed 9 years ago.
I have just entered into iOS programming and am struggling with many things.
I am trying to implement small piece of code to getting current location and send it to server in the background.
When I call beginBackgroundTaskWithExpirationHandler, I found that backgroundTimeRemaining property returns so big number. Look at the log below the code.
if (self.backgroundTask == UIBackgroundTaskInvalid) {
NSLog(#"***** startBackground work");
UIApplication *app = [UIApplication sharedApplication];
self.backgroundTask = [app beginBackgroundTaskWithExpirationHandler:^{
NSLog(#"Background handler called. Not running background tasks anymore.");
[app endBackgroundTask:self.backgroundTask];
self.backgroundTask = UIBackgroundTaskInvalid;
}];
DebugLog(#"====>backgroundTimeRemaining:%.1f seconds", app.backgroundTimeRemaining);
if (timer == nil) {
timer = [NSTimer scheduledTimerWithTimeInterval:10 * 60
target:self
selector: #selector(timerFired:)
userInfo:nil
repeats:YES];
}
}
Log:
2013-11-29 09:23:14.852 JeeneeLocatorService[1666:70b] <JLSViewController.m:(317)> ====>backgroundTimeRemaining:179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0 seconds
It does not look normal and I have read from iOS developer library site iOS allows about 10 minutes even though I call
beginBackgroundTaskWithExpirationHandler to do long time background task.
I am testing with iOS7 simulator in XCode5. Is it true that I can have that much of time for background job. Your answer would be very appreciated.
EDIT:
After getting answer, I move the remaining time display code into the timerFired: method.
- (void)timerFired:(NSTimer *)timer {
DebugLog(#"***** Timer fired *****");
UIApplication *app = [UIApplication sharedApplication];
DebugLog(#"====>backgroundTimeRemaining:%.1f seconds", app.backgroundTimeRemaining);
}
But, it still gives me same time left whenever timerFired: method is called. Does not this work just in simulator?
As your new to ios coding, a little tip:
Hold ALT and click on backgroundTimeRemaining. It tell's you it returns an NSTimeInterval which is a double, so using %f is not the problem.
If you click the bottom blue link of the popup it'll open the docs and you'll see it says that this figure will be very large if the app isn't actually in the background.
So I'm guessing this is your issue, that your app is in the foreground when the NSLog is printed.