59

How do people deal with a scheduled NSTimer when an app is in the background?

Let's say I update something in my app every hour.

updateTimer = [NSTimer scheduledTimerWithTimeInterval:60.0*60.0 
target:self 
selector:@selector(updateStuff) 
userInfo:nil 
repeats:YES];

When in the background, this timer obviously doesn't fire(?). What should happen when the user comes back to the app..? Is the timer still running, with the same times?

And what would would happen if the user comes back in over an hour. Will it trigger for all the times that it missed, or will it wait till the next update time?

What I would like it to do is update immediately after the app comes into the foreground, if the date it should have fired is in the past. Is that possible?

cannyboy
  • 24,180
  • 40
  • 146
  • 252
  • Did you ever figure this out? I'm trying to do the same thing, that is allow a timer to run code when it fires while the app is in the background. – daniel Nov 28 '18 at 15:16

11 Answers11

43

You can have a timer fire while in background execution mode. There are a couple of tricks:

If you are on the main thread:

{
    // Declare the start of a background task
    // If you do not do this then the mainRunLoop will stop
    // firing when the application enters the background
    self.backgroundTaskIdentifier =
    [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{

        [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];
    }];

    // Make sure you end the background task when you no longer need background execution:
    // [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier];

    [NSTimer scheduledTimerWithTimeInterval:0.5
                                     target:self
                                   selector:@selector(timerDidFire:)
                                   userInfo:nil
                                    repeats:YES];
}

- (void) timerDidFire:(NSTimer *)timer
{
    // This method might be called when the application is in the background.
    // Ensure you do not do anything that will trigger the GPU (e.g. animations)
    // See: http://developer.apple.com/library/ios/DOCUMENTATION/iPhone/Conceptual/iPhoneOSProgrammingGuide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html#//apple_ref/doc/uid/TP40007072-CH4-SW47
}

Notes

  • Apps only get ~ 10 mins (~3 mins as of iOS 7) of background execution - after this the timer will stop firing.
  • As of iOS 7 when the device is locked it will suspend the foreground app almost instantly. The timer will not fire after an iOS 7 app is locked.
Community
  • 1
  • 1
Robert
  • 37,670
  • 37
  • 171
  • 213
  • @S_O Yea - I pulled it out of a working test project. Did you have an issue with it? – Robert Jul 08 '13 at 11:48
  • Yes I have the same code, but my timer did not fire at all when my app was in the background. – Sharme Jul 09 '13 at 04:44
  • @S_O What device and iOS version did you test on? How did you test it? You should tap the home button and observe the 'Timer did fire' message in the console. – Robert Nov 11 '13 at 12:39
  • @Robert thanks, it saved me learning many many tutorials, works great. – Tatiana Jan 16 '14 at 09:26
  • Not Working !!! instead of timer you can use while loop for regular calling in background with expiration handler – ManiaChamp Jan 22 '14 at 09:04
  • I have tried using this piece of code & its working on iOS 7 device. I have a question to Robert: Is this acceptable by Apple. ? And also, what if-If I make a service call from within the method `timerDidFire` passing ? I am just wondering that this works even being the app is in BackGround. – Balram Tiwari Feb 24 '14 at 11:09
  • @BalramTiwari - You can use the `@[name]` syntax to comment at someone. This code documented and acceptable by Apple, it doesnt use private APIs and wont be rejected. However, please see my 2 notes at the bottom about limited background execution time. What do you mean by service calls? – Robert Feb 24 '14 at 14:13
  • @Robert :: yea I have read the comments to the bottom of your answer. I am wondering, it ran for more than 10 minutes in background. So with the service call I mean about calling a REST web service block with POST to send the lat long VALUES I get from CLLocation manager in that method. Will that be working for the allowed time as per bottom comments. I hope I have put up the question simpler enough to understand. – Balram Tiwari Feb 24 '14 at 15:32
  • @BalramTiwari - Short answer is I don't know. However, If you want to poll for location updates there the location updates background mode is a better solution. Also how did you get more than 10 mins? Was it 20 mins or 1 hours? Was it on an actual device or the simulator? – Robert Feb 24 '14 at 16:02
  • 1
    @Rob - Good spot. Ill add this in. – Robert Mar 04 '14 at 21:46
  • This example of dispatching the creation of a timer to a background queue, only to schedule it on the main run loop suggests some somewhat confused thinking. Don't conflate background state of the app with the main vs. global queue issue. – Rob Jan 12 '15 at 04:02
  • FWIW, anyone testing techniques such as this, be sure to do final testing with a Release build on an actual device. Re-test when new iOS release: Apple has repeatedly tightened restrictions on repeated background execution, except for specific use cases. See https://developer.apple.com/library/ios/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html And from App Review Guidelines "Multitasking Apps may only use background services for their intended purposes: VoIP, audio playback, location, task completion, local notifications, etc." – ToolmakerSteve May 18 '16 at 22:40
  • @Robert Can you kindly view this question and provide some guideline. Thanks – Ayaz Ali Shah Jul 19 '16 at 12:30
  • I am not sure, but I think apps defined as "media players" or "phone apps" can also run indefinitely in the background (like the Music for instance) and have their timers fire in BG. – Motti Shneor Jul 30 '18 at 12:38
  • It seems like you only get 30 seconds with this on iOS 15+. What gives? – WhiteWabbit Aug 31 '22 at 07:52
43

You shouldn't solve this problem by setting a timer, because you're not allowed to execute any code in the background. Imagine what will happen if the user restarts his iPhone in the meantime or with some other edge cases.

Use the applicationDidEnterBackground: and applicationWillEnterForeground: methods of your AppDelegate to get the behavior you want. It's way more robust, because it will also work when your App is completely killed because of a reboot or memory pressure.

You can save the time the timer will fire next when your App is going to the background and check if you should take action when the App comes back to the foreground. Also stop and start the timer in this methods. While your App is running you could use a timer to trigger the update at the right moment.

Mac_Cain13
  • 3,611
  • 2
  • 24
  • 38
  • Is there no way to let the timer fire while the app is in the background? – daniel Nov 28 '18 at 15:17
  • No there is not, after your app is suspended by iOS there is no way to execute code. Timers will also be suspended and fire only as soon as the app will wake up again. – Mac_Cain13 Nov 28 '18 at 20:36
  • Are there any other ways to run code at a specified time when the app is in the background? – daniel Nov 28 '18 at 20:39
18

In case you or someone else is looking for how to run the NSTimer in the background in Swift, add the following to your App Delegate:

var backgroundUpdateTask: UIBackgroundTaskIdentifier = 0


func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    return true
}

func applicationWillResignActive(application: UIApplication) {
    self.backgroundUpdateTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
        self.endBackgroundUpdateTask()
    })
}

func endBackgroundUpdateTask() {
    UIApplication.sharedApplication().endBackgroundTask(self.backgroundUpdateTask)
    self.backgroundUpdateTask = UIBackgroundTaskInvalid
}

func applicationWillEnterForeground(application: UIApplication) {
    self.endBackgroundUpdateTask()
}

Cheers!

gellieb
  • 213
  • 2
  • 5
7

When in the background, this timer obviously doesn't fire

This post suggests that things aren't quite as clear as that. You should invalidate your timers as your app goes into the background and restart them when it comes back to foreground. Running stuff while in the background might be possible but then again it might get you killed...

You can find good documentation about the iOS multitasking approach here.

Community
  • 1
  • 1
jbat100
  • 16,757
  • 4
  • 45
  • 70
6

On iOS 8 you can get your NSTimer working when the app is in background (even if iphone is locked) up to ~3mins in a simple way:

just implement scheduledTimerWithTimeInterval:target:selector:userInfo:repeats: method as usual, where you need. Inside AppDelegate create a property:

UIBackgroundTaskIdentifier backgroundUpdateTask

then in your appDelegate methods:

- (void)applicationWillResignActive:(UIApplication *)application {
    self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
    [self endBackgroundUpdateTask];
    }];
}

- (void) endBackgroundUpdateTask
{
    [[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];
    self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    [self endBackgroundUpdateTask];
}

after 3 mins the timer will not fire more, when the app will comeback in foreground it will start to fire again

Enrico Cupellini
  • 447
  • 7
  • 14
3

You need to add your timer in current run loop.

[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSRunLoopCommonModes];
Vaibhav Saran
  • 12,848
  • 3
  • 65
  • 75
Maverick King
  • 85
  • 1
  • 10
2
[[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:nil];
    loop = [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(Update) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:loop forMode:NSRunLoopCommonModes];
Murali
  • 500
  • 5
  • 22
vualoaithu
  • 936
  • 10
  • 9
  • 1
    The `addTimer` call is not needed. When you call `scheduledTimerWithTimeInterval`, that schedules the timer on the current run loop already. If you were scheduling this on a background thread, you would call `addTimer`, but, then again, you wouldn't use `scheduledTimerWithTimeInterval` in that case, but rather `timerWithTimeInterval` or the like. But this question wasn't about scheduling a timer from a background thread, but rather how to schedule a timer from the main thread, but to keep that timer going when the app, itself, leaves foreground (which is a _completely_ different issue). – Rob Jan 15 '15 at 12:48
1

You need to add timer in Run loop (Reference - Apple developer page for Run loop understanding).

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self
selector:@selector(updateTimer)  userInfo:nil  repeats:true];

[[NSRunLoop mainRunLoop] addTimer: timer forMode:NSRunLoopCommonModes];

//funcation

-(void) updateTimer{
NSLog(@"Timer update");

}

You need to add permission (Required background modes) of Background working in infoPlist.

byJeevan
  • 3,728
  • 3
  • 37
  • 60
0

create Global uibackground task identifier.

UIBackgroundTaskIdentifier bgRideTimerTask;

now create your timer and add BGTaskIdentifier With it, Dont forget to remove old BGTaskIdentifier while creating new Timer Object.

 [timerForRideTime invalidate];
 timerForRideTime = nil;

 bgRideTimerTask = UIBackgroundTaskInvalid;

 UIApplication *sharedApp = [UIApplication sharedApplication];       
 bgRideTimerTask = [sharedApp beginBackgroundTaskWithExpirationHandler:^{

                            }];
 timerForRideTime =  [NSTimer scheduledTimerWithTimeInterval:1.0
                                                                                 target:self
                                                                               selector:@selector(timerTicked:)
                                                                               userInfo:nil
                                                                                repeats:YES]; 
                                                   [[NSRunLoop currentRunLoop]addTimer:timerForRideTime forMode: UITrackingRunLoopMode];

Here this will work for me even when app goes in background.ask me if you found new problems.

NIRAV BHAVSAR
  • 152
  • 12
0

For me, The background task and run loop was critical and not accurate most of the time.

I decided to use UserDefault approach.

Step 1: Add app enter background/foreground observers

Step 2: When user goes to background, store timer's time in user default with current timestamp

Step 3: When user comes to foreground, compare user default timestamp with current timestamp, and calculate your new timer

All done.

Code snippet:

// Add in viewDidLoad/init functions

NotificationCenter.default.addObserver(self, selector: #selector(self.background(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.foreground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)

// Add in your VC/View

let userDefault = UserDefaults.standard

@objc func background(_ notification: Notification) {
    userDefault.setValue(timeLeft, forKey: "OTPTimer")
    userDefault.setValue(Date().timeIntervalSince1970, forKey: "OTPTimeStamp")
}

@objc func foreground(_ notification: Notification) {
    let timerValue: TimeInterval = userDefault.value(forKey: "OTPTimer") as? TimeInterval ?? 0
    let otpTimeStamp = userDefault.value(forKey: "OTPTimeStamp") as? TimeInterval ?? 0
    let timeDiff = Date().timeIntervalSince1970 - otpTimeStamp
    let timeLeft = timerValue - timeDiff
    completion((timeLeft > 0) ? timeLeft : 0)
    print("timeLeft:", timeLeft) // <- This is what you need
}
Mohammad Zaid Pathan
  • 16,304
  • 7
  • 99
  • 130
0

Swift 4+ version of gellieb's answer

var backgroundUpdateTask: UIBackgroundTaskIdentifier = UIBackgroundTaskIdentifier(rawValue: 0)


func applicationWillResignActive(_ application: UIApplication) {
    self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {
        self.endBackgroundUpdateTask()
    })
}
func endBackgroundUpdateTask() {
    UIApplication.shared.endBackgroundTask(self.backgroundUpdateTask)
    self.backgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
}

func applicationWillEnterForeground(application: UIApplication) {
    self.endBackgroundUpdateTask()
}
Mutawe
  • 6,464
  • 3
  • 47
  • 90