swift NSTimer 在后台

swift NSTimer in Background

我遇到过很多关于如何在后台处理 NSTimer 的问题,这里是在堆栈上或其他地方。我已经尝试了所有实际有意义的选项之一..当应用程序进入后台时停止计时器

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidBecomeActive", name: UIApplicationWillEnterForegroundNotification, object: nil)

一开始我以为我的问题解决了,我只是保存了app确实进入后台的时间和app进入前台的时间差..但是后来我发现时间实际上推迟了3, 4 、 5 秒 .. 它实际上是不一样的 .. 我已经将它与另一台设备上的秒表进行了比较。

运行后台的 NSTimer 真的有可靠的解决方案吗?

遗憾的是,没有可靠的方法可以在后台定期 运行 执行某些操作。您可以使用后台提取,但是 OS 不能保证这些会定期执行。

虽然在后台您的应用程序被挂起,因此没有代码被执行,除了上面提到的后台获取。

你不应该根据它进入后台或恢复的时间进行任何调整,而只是保存你开始或结束的时间(取决于你是向上还是向下计数)。然后当应用程序再次启动时,您只需在重建计时器时使用那个 from/to 时间。

同样,请确保您的计时器处理程序不依赖于调用处理选择器的确切时间(例如 not 执行 seconds++ 之类的操作或类似的操作那是因为它可能不会在您希望的时候准确调用),但总是回到那个 from/to 时间。


这里是一个 count-down 计时器的例子,它说明我们没有 "count" 任何东西。我们也不关心 appDidEnterBackgroundappDidBecomeActive 之间经过的时间。只需保存停止时间,然后计时器处理程序就会比较目标 stopTime 和当前时间,并根据您的需要显示经过的时间。

例如:

import UIKit
import UserNotifications

private let stopTimeKey = "stopTimeKey"

class ViewController: UIViewController {

    @IBOutlet weak var datePicker: UIDatePicker!
    @IBOutlet weak var timerLabel: UILabel!

    private weak var timer: Timer?
    private var stopTime: Date?

    let dateComponentsFormatter: DateComponentsFormatter = {
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.hour, .minute, .second]
        formatter.unitsStyle = .positional
        formatter.zeroFormattingBehavior = .pad
        return formatter
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        registerForLocalNotifications()

        stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
        if let time = stopTime {
            if time > Date() {
                startTimer(time, includeNotification: false)
            } else {
                notifyTimerCompleted()
            }
        }
    }

    @IBAction func didTapStartButton(_ sender: Any) {
        let time = datePicker.date
        if time > Date() {
            startTimer(time)
        } else {
            timerLabel.text = "timer date must be in future"
        }
    }
}

// MARK: Timer stuff

private extension ViewController {
    func registerForLocalNotifications() {
        if #available(iOS 10, *) {
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
                guard granted, error == nil else {
                    // display error
                    print(error ?? "Unknown error")
                    return
                }
            }
        } else {
            let types: UIUserNotificationType = [.alert, .sound, .badge]
            let settings = UIUserNotificationSettings(types: types, categories: nil)
            UIApplication.shared.registerUserNotificationSettings(settings)
        }
    }

    func startTimer(_ stopTime: Date, includeNotification: Bool = true) {
        // save `stopTime` in case app is terminated

        UserDefaults.standard.set(stopTime, forKey: stopTimeKey)
        self.stopTime = stopTime

        // start Timer

        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)

        guard includeNotification else { return }

        // start local notification (so we're notified if timer expires while app is not running)

        if #available(iOS 10, *) {
            let content = UNMutableNotificationContent()
            content.title = "Timer expired"
            content.body = "Whoo, hoo!"
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: stopTime.timeIntervalSinceNow, repeats: false)
            let notification = UNNotificationRequest(identifier: "timer", content: content, trigger: trigger)
            UNUserNotificationCenter.current().add(notification)
        } else {
            let notification = UILocalNotification()
            notification.fireDate = stopTime
            notification.alertBody = "Timer finished!"
            UIApplication.shared.scheduleLocalNotification(notification)
        }
    }

    func stopTimer() {
        timer?.invalidate()
    }

    // I'm going to use `DateComponentsFormatter` to update the
    // label. Update it any way you want, but the key is that
    // we're just using the scheduled stop time and the current
    // time, but we're not counting anything. If you don't want to
    // use `DateComponentsFormatter`, I'd suggest considering
    // `Calendar` method `dateComponents(_:from:to:)` to
    // get the number of hours, minutes, seconds, etc. between two
    // dates.

    @objc func handleTimer(_ timer: Timer) {
        let now = Date()

        if stopTime! > now {
            timerLabel.text = dateComponentsFormatter.string(from: now, to: stopTime!)
        } else {
            stopTimer()
            notifyTimerCompleted()
        }
    }

    func notifyTimerCompleted() {
        timerLabel.text = "Timer done!"
    }
}

顺便说一下,上面还说明了本地通知的使用(以防计时器过期而应用程序当前不在 运行)。


对于 Swift 2 版本,请参阅 previous revision of this answer