Countdown timer, that continue running in background - ios

I need to create timer, that will continue running in background state and will update UI after app will enter foreground state.
Most appropriative example for this is QuizClash app:
I have 2 ideas below:
beginbackgroundTaskWithExpirationHandler
Create first timer (30 sec interval, non-repeatable)
Create second timer (0.01 sec interval, repeatable)
First timer will fire [self timeExiped] method, which will show some UI updates
Second timer will fire update of ProgressBar on the picture above
Wrap it all in beginbackgroundTaskWithExpirationHandler method
Doubts: if main timer will expired in background state, UI updates won't be shown(?)
State restoring
Start the same timers
Use encode​Restorable​State​With​Coder:​​ to save current timestamp, when entering background
Use decode​Restorable​State​With​Coder:​​ to get difference between saved timestamp and now.
Invalidate main timer
Start new main timer with (remains_seconds - time_difference_seconds)
Doubts: is it even a viable option, since i have no experience with it and can't try it out for a moment?
If you have any other idea, or can point me out at 2 described above, i'll put a beer, if you ever in Moscow :)

What is the purpose of the Timer?
If it is to trigger UI changes, there is no point in trying to get it to "run in the background" because there's no UI to update.
With any approach, you will almost certainly want to use timestamps... when the app is returning to the foreground, get the elapsed time since the last timestamp, update your UI as needed, and restart a Timer for foreground activity / updates.

Add an observer for UIApplicationDidBecomeActive to the ViewController containing the ProgressBar in viewWillAppear:
NotificationCenter.default.addObserver(forName: NSNotification.Name.UIApplicationDidBecomeActive, object: nil, queue: nil, using: self.applicationBecameActive)
Put it in viewWillAppear because the applicationBecameActive function should only fire when the viewcontroller containing the progress bar is on the top of the stack when the application becomes active again.
Then in the applicationBecameActive method, determine what the current progress value of the ProgressBar should be, update accordingly, and restart the ProgressBar progressing.
func applicationBecameActive(notification: Notification){
//determine where the ProgressBar should be
//set the ProgressBar progress value
//restart the ProgressBar progress value updating
}
Don't forget to remove the observer on viewWillDissappear:
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)

Related

Why timer invoke its block so quickly sometimes

I create a timer and invoke its block every 5 second. Then I make application to enter background and enter foreground after a while. But it could invoke the block quickly sometimes.
let _ = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { (timer) in
print("--------")
}
When I enter foreground the interval of first printing and second printing may less than a second sometimes. Is time interval invalid in this case?
To understand the behavior, you need to understand how NSTimer and RunLoop works. In simple terms, a RunLoop would check if the Timer should fire, if yes it would notify the Timer to fire the selector, else it won't. Now, since you are on the background, your RunLoop is not checking for events so it won't be able to notify the Timer. But once it goes to foreground, it would see that it would need to notify the Timer even if it passed the fireDate.
TimeLine Diagram:
Let A(5th second) and B(10th second) be timer fire events. Scheduled on a timer Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true)
C is entered background (0 seconds)
D be coming back to foreground (9th second, between A and B).
-----> A ------> B
C--------->D
Explanation:
On C, the RunLoop would be paused. Therefore Event A is not able to be processed until the RunLoop has resumed processing, which is on Event D. Upon Event D, it will see that Event A should fire so it would notify the Timer. After a second, the RunLoop would see that Event B has happened so it would notify the Timer again. This scenario explains why your events are printing in a second's interval. It is just the delayed event handling that makes it seem that it fired earlier, when in reality it was processed late.
Apple Doc:
A timer is not a real-time mechanism. If a timer’s firing time occurs
during a long run loop callout or while the run loop is in a mode that
isn't monitoring the timer, the timer doesn't fire until the next time
the run loop checks the timer. Therefore, the actual time at which a
timer fires can be significantly later.
resources: What is an NSTimer's behavior when the app is backgrounded?, NSRunLoop and Timer Docs
Suggestions:
Stop your timer once app goes background, but store the fireDate. Once coming back to foreground, check if fireDate is past Date(). Then create a new Timer to handle events while on foreground.
When application entered background, the app would be suspended soon, program stopped running. When app switched back to foreground, the buffered/delayed timer event would be fired, then you saw many prints quickly.

How to keep a timer counting when app reach background

I prepared a CountDown timer for Pomodoro technique. I would like to know how don't pause the app when it reach a background. I have a method which update UILabel from 20min to 0 by 1sec. When Timer reach 0 it should play the sound and vibrate device. All works fine when app is launched in foreground, but how to do it at background? Is it possible to track timer change when app is in background mode?
BR
iMat
The short answer is no. A timer on a VC will not continue to run when the app is in the background because it goes into suspended mode.
You could schedule a local notification to fire when the app is in the background, but as far as updating the UI label, you'll have to update that when the user comes back into the app.
Invalidate the timer when the app goes to background. Store the remaining time remainingTime and current time backgroundTime. (You can get the current time using Date())
Compare the current time backToForegroundTime when the app comes back with backgroundTime. Subtract them to get the time elapsed timeElapsed.
If timeElapsed is less than the remainingTime, subtract that amount from remainingTime and create the timer again with the new duration.
You can use my approach from this gist. Just create repeating timer and update what ever you want in repeating block, and handle timer finishing in other block on main queue or background queue!
Glad to help with questions!
Apple has defined a specific set of tasks, an app can perform when in background.
Running a timer, unfortunately, is not one of them.
Read Background Execution section of app programming guide for more details.
Most apps, intending to continue to execute code in background, implement one of the allowed long running background modes, even if it is not required for your apps actual functionality, and use them to execute their code.
But be ware, you will be doing something apple specifically asks you not to do. This could result in app store rejection if found.

How to set a timer to run when an app is both in foreground and in background?

I've my app set to keep receiving location updates while in background ("Background modes > Location updates"). My app listens for location updates while in foreground as well. But I need my app to keep listening for locations during a certain number of seconds I'm given, and to show a countdown if the app is in foreground, and to keep the time count if the app goes to background because I need to stop the location manager when the time ends.
I found no problems in setting an NSTimer when the app is in foreground, and showing a countdown, but I don't know how to handle it when app goes to background... in comments in AppDelegate's delegate applicationWillResignActive: and applicationDidEnterBackground: methods it is said that timers should be disabled and invalidated there... in addition, the timer I'm using is created in main thread:
[NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:#selector(updateCountdown:)
userInfo:nil
repeats:YES];
How could/should I manage this scenario?
Thanks in advance
EDIT: I'm going to try to explain an scenario I could have: my app listens for locations during a given time interval, and then it should stop listening. Let's say I'm going to give the user 5 min to reach certain location. User taps a kind of "start" button, and then I start the location manager to keep track of the user's positions, and I also start a countdown. When the countdown reaches 0, I stop the location manager and check the path the user has followed and the location he has reached. Since the app could go to background after the user has started the countdown, I have enabled Background modes > Location updates. Listening for locations while the app is in background is ok but, what if the countdown finishes while the app is still in background? I need to stop tracking user's positions even if the app is in background. I need a timer to keep running in background whatever countdown I need to set for the user, it could be 5 min, it could be 1 hour...
Thanks again
You can't run a timer in the background forever, it's not one of the background modes allowed by the OS so it will get terminated at some point. Wrapping it in a background task will only buy you a few extra minutes (about 10) at the most.
To properly track time when your app enters the background you need to:
From applicationDidEnterBackground
Save off a time stamp as soon as your app transitions to the background.
Terminate your timers
When your app re-enters the foreground:
From applicationWillEnterForeground edit or 'didUdateLocations'
Read the save time stamp
Use timeIntervalSinceDate to get the lapsed time in seconds
Restart your timer using the time span to set the remaining time
I think this kind of control needs a middleware to operate perfectly. I don't know if your app has backend or not but if it has, you may send a request when user tries to start timer in your app and you do your updates background of your app. After timer finishes backed can send a notification to user about this and you handle it if user comes back to your app or you leave it that way.

Keeping timers running when app enters background

I am making an app that relies on a timer that fires every minute to change a label from "x minutes left" to "(x-1) minutes left. Basically, it decrements the number every minute. I need the timer to function even when I close the app (not entirely, just press the home screen and leave it running in the background), so that when the user comes back to the app after leaving it in the background for 5 minutes, the label will say "(x-5) minutes left".
Right now, when I run it in the simulator it works perfectly, but when I run it on my phone it does not work. If I have the app open on my phone, it works, but if the app is running in the background, the label never decrements. Could this be due to differences in the way the simulator and actual iPhone handle multitasking? If so, how can I change my code so that the timer will still update the label every minute so that the correct number is displayed when the user reopens the app?
Here is my timer setup:
var individualTaskTimer = NSTimer()
func createTimerForTopTask(){
individualTaskTimer = NSTimer.scheduledTimerWithTimeInterval(tasks[0].minutes * 60, target: self, selector: "deleteTopTask", userInfo: nil, repeats: true)
}
As mentioned in Ewan Mellor's answer, you will not be able to rely on a timer while the app is in the background. So you will need to adjust as necessary when your app returns to the foreground.
Upon first reading the documentation, it might seem like viewWillAppear and viewWillDisappear (or viewDidDisappear) are the correct places to handle this. However, they do not get called when the app moves to/from the background.
Instead, you can make use of two notifications, UIApplicationWillResignActiveNotification and UIApplicationDidBecomeActiveNotification. The first notification will be sent to your app when it is about to go into the background. The second notification will be sent to your app when it is about to return to the foreground.
So in viewWillAppear you can register for the notifications as follows:
override func viewWillAppear(animated: Bool) {
// some other code
NSNotificationCenter.defaultCenter().addObserver(self, selector: "activeAgain", name: "UIApplicationDidBecomeActiveNotification", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "goingAway", name: "UIApplicationWillResignActiveNotification", object: nil)
}
where activeAgain and goingAway are two functions you've written to enable and disable the timer. So based on the code snippet in your question, they would look something like this:
func activeAgain() {
let newTime = // calculate how much time is left (in seconds)
individualTaskTimer = NSTimer.scheduledTimerWithTimeInterval(newTime, target: self, selector: "deleteTopTask", userInfo: nil, repeats: true)
}
func goingAway() {
individualTaskTimer.invalidate()
}
Note that you need to unregister for the notifications when you switch away from this view. Doing this in viewWillDisappear is probably a good spot.
You can't do this. iOS will suspend your app after a short while when it goes into the background. Just update your label with the correct time when the app comes back to the foreground.

Keep running NSTimer switching Background and Foreground

I am trying to create a NSTimer which is started manually by the user in foreground, and as expected works perfectly:
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: nil, repeats: true)
where updateCounter() simply updates a UILabel in the UI.
Now what I want to achieve is to get it running also when the user leaves the application. I googled something and found this code (which I translated in Swift):
func applicationDidEnterBackground(application: UIApplication) {
var bgTask = UIBackgroundTaskIdentifier()
bgTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler { () -> Void in
UIApplication.sharedApplication().endBackgroundTask(bgTask)
}
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
var vc = TimerViewController()
// Fire some methods
UIApplication.sharedApplication().endBackgroundTask(bgTask)
bgTask = UIBackgroundTaskInvalid
});
}
Basically from what I can understand here is that when app reaches background I can fire some methods from a new instance of my TimerViewController(), which is supposed to handle the NSTimer for the background phase.
Now does this imply that I need to use a method from this vc, or another, which starts another NSTimer starting from the exact time in which the foreground-one was stopped (because app entered background)?
If yes, to resume my first NSTimer when the user opens the app again (meaning foreground again :D) how should I behave? Stop the second NSTimer and resume the first with updated counter?
Actually even a sketch of the solution is appreciated, I'm just trying to figure out how to handle the situation.
further question: If what it's written above turn out to be correct how can I manage the instances of the viewcontroller to get the timer_value(token for something like secondsLeft) when a switch from background to foreground or vice-versa happens?
Instead of trying to maintain an NSTimer in the background (which is impossible beyond about 10 minutes), what you want to do is record when your timer started and save that in NSUserDefaults. You can still update your timer every second, but just recalculate the label based on the start time (remember, NSTimer doesn't promise that it will run on time; it could be late for many reasons and you'll drift).
Now there is no reason to run in the background. When your view controller comes onscreen (viewDidAppear:), just update the label again, based on the start time saved in NSUserDefaults.
There is no way in iOS to run indefinitely in the background. That's by design. But there's also no reason for you to be wasting system resources trying to update your UI when you're not onscreen. Just update it when you are.

Resources