Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simple service in Swift, persists, foreground only?

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
    }
}
like image 587
Fattie Avatar asked Jul 07 '17 02:07

Fattie


People also ask

How do you check if app is in foreground iOS Swift?

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.

What is foreground service in iOS?

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.


1 Answers

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!

like image 127
Daniel Avatar answered Nov 15 '22 20:11

Daniel