用Swift编写的简单服务,仅在前台持续存在?

13

我只是想要一个简单的东西在iOS上运行

  • 每20秒运行一次
  • 只在应用程序处于前台时运行(明确地说,不在后台运行)
  • 显然,如果您不必跟踪应用程序进出前台,那将更加优雅
  • 问题在于,我惊讶地发现,在模拟器中使用 scheduleRepeating 时,当应用程序在后台时它会继续运行:这让我不太确信它在某些设备上明确地不会在后台运行
  • 我发现,在设备上测试尽可能多的OS版本和代数时,scheduleRepeating 烦人地只会(在某些或所有情况下)运行一次,并且仅在应用程序进入后台时运行一次。也就是说:它会在应用程序到达后台时再运行一次。真烦人。

因此,如何在iOS上实现一个简单的服务,每20秒运行一次,并且只在应用程序在前台时运行,而且最好是无需重新启动、检查或处理整个应用程序生命周期中任何内容的方法......

真的有没有更好的方法来完成这么简单的事情呢?

这是我的(看起来)可行的解决方案,但不够优雅。在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
当您进入后台时,使计时器失效并记录剩余时间,然后在返回前台时重新安排计时器。 - Alexander
让我们在聊天中继续这个讨论 - Alexander
你有机会测试我的答案了吗? - Daniel
嗨@Dopapp,老实说,只是使用我展示的DispatchSourceTimer已经更加整洁了。 - Fattie
啊,当你说“看起来工作正常”时,我以为它有时会出问题。 - Daniel
显示剩余3条评论
2个回答

4

幕后:

虽然它不是很华丽,但它的运行非常出色。

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()
}

这段代码现成可用。当然,您可以将 timeInterval 更改为任何时间间隔。

使用方法简单:

在 AppDelegate.swift 中,添加这两个方法:

func applicationDidBecomeActive(_ application: UIApplication) {
    ForegroundTimer.startTimer()
}

func applicationDidEnterBackground(_ application: UIApplication) {
    ForegroundTimer.pauseTimer()
}

然后,让想要“监听”计时器的任何类实现ForegroundTimerDelegate及其所需的方法timeDidPass()。最后,将该类/self分配给ForegroundTimer.sharedTimerDelegate。例如:

class ViewController: UIViewController, ForegroundTimerDelegate {
    func viewDidLoad() {
        ForegroundTimer.sharedTimerDelegate = self
    }

    func timeDidPass() {
        print("20 in-foreground seconds passed")
    }
}

希望这能帮到你!

正如我所说,“显然,如果您不必跟随应用程序进出前台,那将更加优雅”。我提供的DispatchSourceTimer示例已经比这个简单得多了 :/ 我想要比我已经给出的示例代码更简单的东西,呵呵! - Fattie
@Fattie,也许可以在代码高尔夫上发布它:p </半开玩笑> - Daniel
1
这是一个很好的答案。虽然我没有测试过它,但是阅读它时,感觉非常可靠。它可能不是“简单”的(也不复杂)...但它肯定是一种设置并忘记的东西,只需要在想要更改间隔时注意一下即可。 - Jacob Boyd
嘿@JacobBoyd,我在问题中已经给出的答案比这个好得多(更简单、更可靠、更现代、更省力、更短、需要更少的调用!!!)。正如我之前所说,注意第4和第5个要点。我们需要一个真正精通iOS运行循环和应用程序周期的专家,来给出这些问题的明确答案!! - Fattie

1

@Dopapp的回答非常可靠,我会同意他的建议。但如果你想要更简单一些的方式,你可以尝试使用RunLoop:

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

这甚至可以为您提供一些关于后台执行的功能。 警告:代码未经测试!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接