I very simply want something to run
scheduleRepeating
that, in the simulator, it keeps running when the app is in background: that doesn't give me confidence that it explicitly won't run in background on (some? whatever?) devicesscheduleRepeating
annoyingly will (in some? all? cases) run one time only when the app goes into background. That is to say: it runs "one more time" once the app goes to background. Suck.So to repeat: in iOS how to have a simple service that runs every 20 seconds, and, only runs when the app is in a foreground, and, it is fuss-free .. ideally you don't have to restart it, check it or anything during the life of the app...
really what is the best way to do such a simple thing?
This is my (seemingly) working solution, which is inelegant. Surely there is a built-in way, or something, to do such an obvious thing in iOS??
open class SomeDaemon {
static let shared = SomeDaemon()
fileprivate init() { print("SomeDaemon is running") }
var timer: DispatchSourceTimer? = nil
let gap = 10 // seconds between runs
func launch() {
// you must call this from application#didLaunch
// harmless if you accidentally call more than once
// you DO NOT do ANYTHING on app pause/unpause/etc
if timer != nil {
print("SomeDaemon, timer running already")
return
}
timer = DispatchSource.makeTimerSource(flags: [], queue: DispatchQueue.main)
timer?.scheduleRepeating(deadline: .now(), interval: .seconds(gap))
timer?.setEventHandler{ self. doSomeThing() }
timer?.resume()
}
private func doSomeThing() {
if UIApplication.shared.applicationState != .active {
print("avoided infuriating 'runs once more in bg' problem.")
return
}
// actually do your thing here
}
}
To detect if an iOS application is in background or foreground we can simply use the UIApplication just like we can use it to detect many other things like battery state, status etc.
If an app is in the foreground when a local/remote notification fires then the app receives it directly. If the app is not currently in the foreground when a local/remote notification fires then the app DOES NOT receive it.
It isn't gorgeous, but it works pretty darn well.
struct ForegroundTimer {
static var sharedTimerDelegate: ForegroundTimerDelegate?
static var sharedTimer = Timer()
private var timeInterval: Double = 20.0
static func startTimer() {
ForegroundTimer.waitingTimer.invalidate()
var wait = 0.0
if let time = ForegroundTimer.startedTime {
wait = timeInterval - (Date().timeIntervalSince1970 - time).truncatingRemainder(dividingBy: Int(timeInterval))
} else {
ForegroundTimer.startedTime = Date().timeIntervalSince1970
}
waitingTimer = Timer.scheduledTimer(withTimeInterval: wait, repeats: false) { (timer) in
ForegroundTimer.sharedTimerDelegate?.timeDidPass()
ForegroundTimer.sharedTimer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: { (timer) in
ForegroundTimer.sharedTimerDelegate?.timeDidPass()
})
}
}
static func pauseTimer() {
ForegroundTimer.sharedTimer.invalidate()
}
private static var startedTime: TimeInterval?
private static var waitingTimer = Timer()
}
protocol ForegroundTimerDelegate {
func timeDidPass()
}
This code above is ready to use as-is. Of course you can timeInterval
to whatever time interval you want.
In AppDelegate.swift, have these two methods:
func applicationDidBecomeActive(_ application: UIApplication) {
ForegroundTimer.startTimer()
}
func applicationDidEnterBackground(_ application: UIApplication) {
ForegroundTimer.pauseTimer()
}
Then, have whatever class you want to 'listen' for the timer implement ForegroundTimerDelegate
, and it's required method, timeDidPass()
. Finally, assign that class/self
to ForegroundTimer.sharedTimerDelegate
. For example:
class ViewController: UIViewController, ForegroundTimerDelegate {
func viewDidLoad() {
ForegroundTimer.sharedTimerDelegate = self
}
func timeDidPass() {
print("20 in-foreground seconds passed")
}
}
Hope this helps!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With