How to setup long running timers on iOS - ios

I want to create a timer that should fire every hour. But from my research, an app gets suspended after 10 mins in the background. It also seems like the app gets suspended after the screen is locked.
I want to trigger this timer every 1 hour. I will be invalidating the timer when app goes in the background and restart it when the app foregrounds. So I have a few questions:
When the user backgrounds the app and comes back to it, will the timer fire immediately if it has been 1+ hour?
What happens if the user returns to the app after multiple (2+) hours, will the timer fire multiple times?
Are there any recommended ways to setup such longer running timers so they fire more consistently and not just once when they were setup?

You can do something like this without using background timer. It is only idea how you can achieve your requirement, add one or more hours condition as per your requirement.
var totalTime = Double()
override func viewDidLoad() {
super.viewDidLoad()
// MARK: - To Reset timer's sec if app is in background and foreground
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)
}
#objc func background(_ notification: Notification) {
if self.totalTime > 0{
user_default.setValue(self.totalTime, forKey: "TotalSecInBackground")
user_default.setValue(Date().timeIntervalSince1970, forKey: "OldTimeStamp")
LogInfo("total seconds left in background: \(self.totalTime)")
}
}
#objc func foreground(_ notification: Notification) {
let timerValue: TimeInterval = user_default.value(forKey: "TotalSecInBackground") as? TimeInterval ?? 0
let otpTimeStamp = user_default.value(forKey: "OldTimeStamp") as? TimeInterval ?? 0
let timeDiff = Date().timeIntervalSince1970 - otpTimeStamp
if timerValue > timeDiff{
LogInfo("total second & timeDiff:, \(Int(timerValue)),\(Int(timeDiff))")
let timeLeft = timerValue - timeDiff
self.totalTime = Int(timeLeft)
LogInfo("timeLeft: \(Int(timeLeft))") // <- This is what you need
}}

Related

timer gets called after coming back from minimized state iOS

I build a real time clock after fetching current location and then shows current time from api response - I'm using these function to display current time.
func getCurrentTime() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.currentTimeAfterFetchedTime), userInfo: nil, repeats: true)
}
#objc func currentTimeAfterFetchedTime(currentTime : Int) {
print("Timer Function gets Called \(Date())")
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, h:mm:ss a"
DispatchQueue.main.async {
self.presentDayDateNTime.text = formatter.string(from: Date(timeIntervalSince1970: TimeInterval(self.dynamicCurrentDateNTime)))
self.dynamicCurrentDateNTime += 1
}
}
Now I want to refetch api and show real time if user come back after minimized state. So I added this Notification observer to check if the app comes back from minimized state or not -
NotificationCenter.default.addObserver(self, selector: #selector(applicationComesInTheForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
In my viewDidLoad() and also define this to fetch location and call api again-
#objc func applicationComesInTheForeground() {
print("Called")
self.spinner.startAnimating()
fetchCurrentLocation()
}
My app works fine when starts first time but when it coming back from minimize state the currentTimeAfterFetchedTime gets called doubled in a seconds and my clock gets fast enough to count 1 minute in just 30 seconds.
I'm calling currentTimeAfterFetchedTime function from completionhandler of api call-
DispatchQueue.main.async {
print("In Dispathch",self.currentDayData)
// MARK: - Dynamic time representation afetr fetching data from API
self.dynamicCurrentDateNTime = self.currentDayData.dt
self.getCurrentTime()
}
So, My question is why my timer function gets called double in a seconds?
Add the following observer and invalidate the timer whenever the app goes to background or inactive state.
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillBecomeInactive), name: UIApplication.willResignActiveNotification, object: nil)
#objc func applicationWillBecomeInactive() {
print("application inactive")
timer.invalidate()
}
Whenever the App becomes active you can start the timer (as done by you)
You should reset the old timer in getCurrentTime() before the start it.
timer.invalidate()

Timer disrupts when notification center is pulled down in iOS 12

So I am creating an app that has countdown timer. When the app quits I am using observers to know if the app is in background. If it is, I invalidate the timer and store the quit time in userDefaults. Then when the app comes back to foreground, I create a new timer and calculate the time that the app has been in background and subtract it from the total duration in order to get the elapsed time. When app goes to the background, I am storing the time in UserDefaults:
#objc func applicationDidEnterBackground() {
let defaults = UserDefaults.standard
let quitTime = Date()
defaults.set(quitTime, forKey: "quitTimeKey") //Storing the time of quit in UserDefaults
timer?.invalidate()
}
Then I create a new instance of timer when app enters foreground:
#objc func appEntersForeground() {
calculateTimeLeft()
if (timer == nil)
{
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(handleCountdown), userInfo: nil, repeats: true)
}
}
Then I check the elapsed time:
func checkElapsedTime() -> Double {
let currentTime = Date()
let appQuitTime = UserDefaults.standard.object(forKey: "quitTimeKey") as? Date ?? Date.distantFuture
let elapsedTime = currentTime.timeIntervalSince(appQuitTime)
return elapsedTime
}
I am also printing the time difference:
let timeDifference = checkElapsedTime()
print("timeDifference = \(timeDifference)")
Question: However, here is an issue. When I am using the app and I slide the notification center down and back up for not even a second, I get a timeDifference reading of few thousand seconds.
What could be the reason here? Is this iOS 12 bug? This only happens when I pull the notification center down when I am in the app.
Alright so I got it working. Basically when you are sliding down the notification center, you are calling applicationWillResignActive. So instead of calling applicationDidEnterBackground, I used applicationWillResignActive for the notification and it started working all fine!

Timer is not running more than 3 minutes in background

I am working on meditation app. In this app i have some musical content and some silent meditation section using timer. Timer is working fine when it is in foreground but it is running for only 3 min in background(when device is locked or user press home button to exit from the app). I am using swift4. What i have tried :
var timer: Timer!
var timeCounter : Int!
var backgroundTaskIdentifier: UIBackgroundTaskIdentifier?
var backgroundTask = BackgroundTask()
#IBOutlet weak var timeLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
self.backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: {
UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier!)
})
backgroundTask.startBackgroundTask()
DispatchQueue.global(qos: .background).async {
let currentRunLoop = RunLoop.current
let timeInterval = 1.0
self.timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.updateTimer), userInfo: nil, repeats: true)
self.timer.tolerance = timeInterval * 0.1
currentRunLoop.add(self.timer, forMode: .commonModes)
currentRunLoop.run()
}
}
}
#objc func updateTimer () {
timeCounter = timeCounter - 1
let minutes = Int(timeCounter) / 60 % 60
let seconds = Int(timeCounter) % 60
print("timeCounter", timeCounter)
if (timeCounter == 0){
if self.timer != nil {
self.timer?.invalidate()
self.timer = nil
player.play()
}
}
timeLabel.fadeTransition(0.4)
timeLabel.text = String(format: "%02i:%02i",minutes,seconds)
}
Thanks in Advance.
In general you get limited time from the OS to run in background. You can check and react to the background time left using:
UIApplication.shared.backgroundTimeRemaining
If conditions are good (device is unlocked, battery full ...) you typically get about 160-180 seconds.
You find detailed information in Apples documentation.
As you want to play audio, you can use "Plays Audio" background mode to not get cut by the OS:
Depending how you play audio, configuring the AudioSession might also improve things.
Edit:
How I understand now from your comment, you want your app to do something every 4 minutes. The only possiblility I see is to use the BackgroundFetch feature. This does not guarantee a fixed interval though.

Swift: Timer run in foreground/background

I'm learning how to create a Pomodoro app, and am able to send notifications. However, I am totally clueless as to how to allow my timer label to update itself on reloading the app. Which means the timer works only when the app is open and not when it's in the foreground/background. Hoping to find a tutorial to learn from or just a quick answer code. Thanks!
Edit: Just to clear some misunderstandings, my app's Notification works fine with the timer, for example if 30mins is selected, the app would notify the user after 30mins. However, the problem is that when the app reopens, it resumes for example 29:57 seconds left on the timer label while the 30mins should have passed already.
*Added in AppDelegate*
var seconds = 0 //Timer countdown seconds
var currentDate = NSDate()
var setDate: Int = 0
func pauseApp(){
viewC.timer.invalidate() //invalidate timer
UserDefaults.standard.set(seconds, forKey: "current") //error occurs here where "Cannot assign value of type NSDate to type Timer"
setDate = UserDefaults.standard.integer(forKey: "current")
}
func startApp(){
let difference = currentDate.timeIntervalSince(NSDate() as Date) as Double
seconds = Int(Double(setDate) - difference)
viewC.updateTimer()
}
What someone suggests from a different thread is cancel the timer and store a NSDate when the app goes to the background. He stated we can use this notification to detect the app going to the background:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "pauseApp", name: UIApplicationDidEnterBackgroundNotification, object: nil)
Then cancel the timer and store the date:
func pauseApp(){
self.stop() //invalidate timer
self.currentBackgroundDate = NSDate()
}
Use this notification to detect the user coming back:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "startApp", name: UIApplicationDidBecomeActiveNotification, object: nil)
Then calculate the difference from the stored date to the current date, update your counter and start the timer again:
func startApp(){
let difference = self.currentBackgroundDate.timeIntervalSinceDate(NSDate())
self.handler(difference) //update difference
self.start() //start timer }
However, I do not fully understand this code (namely, the difference between the "handler" and my own "seconds") as am new to programming... Hoping for an answer or helpful insight.
Solved: I managed to solve it myself from this video https://www.youtube.com/watch?v=V6ta24iBNBQ
Using this concept of timeDifference as well as UserDefaults.standard.set....
I managed to adapt it to my personal app with the code
You can call Timer to run the timmer when the view loads.
var runTimer : Timer?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
runTimer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(myFun), userInfo: nil, repeats: true)
}
func myFun(){
//do your logic
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
runTimer?.invalidate()
}

Swift 3 - How to make timer work in background

i am trying to do an application which can make a timer run in background.
here's my code:
let taskManager = Timer.scheduledTimer(timeInterval: 10, target: self, selector: #selector(self.scheduleNotification), userInfo: nil, repeats: true)
RunLoop.main.add(taskManager, forMode: RunLoopMode.commonModes)
above code will perform a function that will invoke a local notification.
this works when the app is in foreground, how can i make it work in the background?
i tried to put several print lines and i saw that when i minimize (pressed the home button) the app, the timer stops, when i go back to the app, it resumes.
i wanted the timer to still run in the background. is there a way to do it?
here's what i want to happen:
run app -> wait 10 secs -> notification received -> wait 10 secs -> notification received -> and back to wait and received again
that happens when in foreground. but not in background. pls help.
you can go to Capabilities and turn on background mode and active Audio. AirPlay, and picture and picture.
It really works . you don't need to set DispatchQueue .
you can use of Timer.
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (t) in
print("time")
}
Swift 4, Swift 5
I prefer to not run timer on background task, just compare a Date seconds between applicationDidEnterBackground and applicationWillEnterForeground.
func setup() {
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func applicationDidEnterBackground(_ notification: NotificationCenter) {
appDidEnterBackgroundDate = Date()
}
#objc func applicationWillEnterForeground(_ notification: NotificationCenter) {
guard let previousDate = appDidEnterBackgroundDate else { return }
let calendar = Calendar.current
let difference = calendar.dateComponents([.second], from: previousDate, to: Date())
let seconds = difference.second!
countTimer -= seconds
}
This works. It uses while loop inside async task, as suggested in another answer, but it is also enclosed within a background task
func executeAfterDelay(delay: TimeInterval, completion: #escaping(()->Void)){
backgroundTaskId = UIApplication.shared.beginBackgroundTask(
withName: "BackgroundSound",
expirationHandler: {[weak self] in
if let taskId = self?.backgroundTaskId{
UIApplication.shared.endBackgroundTask(taskId)
}
})
let startTime = Date()
DispatchQueue.global(qos: .background).async {
while Date().timeIntervalSince(startTime) < delay{
Thread.sleep(forTimeInterval: 0.01)
}
DispatchQueue.main.async {[weak self] in
completion()
if let taskId = self?.backgroundTaskId{
UIApplication.shared.endBackgroundTask(taskId)
}
}
}
}
A timer can run in the background only if both the following are true:
Your app for some other reason runs in the background. (Most apps don't; most apps are suspended when they go into the background.) And:
The timer was running already when the app went into the background.
Timer won't work in background. For background task you can check this link below...
https://www.raywenderlich.com/143128/background-modes-tutorial-getting-started
============== For Objective c ================
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.
You can achieve this by getting the time-lapse between background and foreground state of the app, here is the code snippet.
import Foundation
import UIKit
class CustomTimer {
let timeInterval: TimeInterval
var backgroundTime : Date?
var background_forground_timelaps : Int?
init(timeInterval: TimeInterval) {
self.timeInterval = timeInterval
}
private lazy var timer: DispatchSourceTimer = {
let t = DispatchSource.makeTimerSource()
t.schedule(deadline: .now() + self.timeInterval, repeating: self.timeInterval)
t.setEventHandler(handler: { [weak self] in
self?.eventHandler?()
})
return t
}()
var eventHandler: (() -> Void)?
private enum State {
case suspended
case resumed
}
private var state: State = .suspended
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.willEnterForegroundNotification, object: nil)
timer.setEventHandler {}
timer.cancel()
resume()
eventHandler = nil
}
func resume() {
NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForegroundNotification), name: UIApplication.willEnterForegroundNotification, object: nil)
if state == .resumed {
return
}
state = .resumed
timer.resume()
}
func suspend() {
if state == .suspended {
return
}
state = .suspended
timer.suspend()
}
#objc fileprivate func didEnterBackgroundNotification() {
self.background_forground_timelaps = nil
self.backgroundTime = Date()
}
#objc fileprivate func willEnterForegroundNotification() {
// refresh the label here
self.background_forground_timelaps = Date().interval(ofComponent: .second, fromDate: self.backgroundTime ?? Date())
self.backgroundTime = nil
}
}
Use this class like;
self.timer = CustomTimer(timeInterval: 1)
self.timer?.eventHandler = {
DispatchQueue.main.sync {
var break_seconds = self.data.total_break_sec ?? 0
break_seconds += 1
if self.timer?.background_forground_timelaps != nil && self.timer?.backgroundTime == nil{
break_seconds += (self.timer?.background_forground_timelaps)!
self.timer?.background_forground_timelaps = nil
}
self.data.total_break_sec = String(break_seconds)
self.lblBreakTime.text = PRNHelper.shared.getPlainTimeString(time: TimeInterval(break_seconds))
}
}
self.timer?.resume()
This way I am able to get the timer right when resumed the app from background.
If 1 or 2 seconds threshold is acceptable this hack could be helpful.
UIApplication.didEnterBackgroundNotification
UIApplication.willEnterForegroundNotification
Stop Timer and Backup Date() on didEnterBackground.
Add Date() to the Backup date on willEnterForegraound to achieve total time.
Start Timer and Add total date to the Timer.
Notice: If user changed the date time of system it will be broken!
You dont really need to keep up with a NSTImer object. Every location update comes with its own timestamp.
Therefore you can just keep up with the last time vs current time and every so often do a task once that threshold has been reached:
if let location = locations.last {
let time = location.timestamp
guard let beginningTime = startTime else {
startTime = time // Saving time of first location time, so we could use it to compare later with subsequent location times.
return //nothing to update
}
let elapsed = time.timeIntervalSince(beginningTime) // Calculating time interval between first and second (previously saved) location timestamps.
if elapsed >= 5.0 { //If time interval is more than 5 seconds
//do something here, make an API call, whatever.
startTime = time
}
}
As others pointed out, Timer cannot make a method run in Background. What you can do instead is use while loop inside async task
DispatchQueue.global(qos: .background).async {
while (shouldCallMethod) {
self.callMethod()
sleep(1)
}
}

Resources