-->

Simple service in Swift, persists, foreground only

2020-08-12 17:43发布

问题:

I very simply want something to run

  • every say 20 seconds
  • simply only when the app is foreground (explicitly not run when background)
  • obviously, it would be more elegant if you don't have to bother following the app going in and out of foreground
  • The issue, I was amazed to learn with 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?) devices
  • The issue, I discovered that on a device, testing with as many generations / OS as possible, scheduleRepeating 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
    }
}

回答1:

Behind the scenes:

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.

Usage is simple:

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!



回答2:

@Dopapp's answer is pretty solid and I'd go with it but if you want it even simpler you can try something with the RunLoop:

let timer = Timer.scheduledTimer(withTimeInterval: wait, repeats: true) { _ in print("time passed") }
RunLoop.current.add(timer, forMode: RunLoopMode.commonModes)

This will even give you some functionalities about background execution.

WARNING: Code not tested!