如何在后台模式下 运行 NStimer?

how to run NStimer in background Mode?

我正在开发计算员工工作时间的应用程序。
所以我想 运行 一个在后台模式下保持活动状态的计时器,但我没有找到任何直接执行此操作的方法。

我想我可以通过以下策略实现这一目标...

  1. applicationDidEnterBackground方法中userDefault保存当前时间。
  2. 当应用程序再次激活时,从 userDefault 中获取保存的时间,然后在 applicationWillEnterForeground 中找到与当前时间的差异 method.Then 显示计算的时间。

现在,我想知道这样做是否正确?或者有什么好的方法吗??
谢谢!!

没有理由将当前时间保存在 applicationDidEnterBackground

您真正需要的只是用户“开始工作”的时间,或者 nil 如果用户尚未开始工作。每当您需要知道工作时间时,您可以将其计算为当前时间减去 started-work 时间:

let workStartTimeKey = "workStartTime"

class ViewController: UIViewController {

    var secondsWorked: CFTimeInterval? {
        guard UserDefaults.standard.object(forKey: workStartTimeKey) != nil else { return nil }
        let startTime = UserDefaults.standard.double(forKey: workStartTimeKey)
        return CFAbsoluteTimeGetCurrent() - startTime
    }

(由于iOS SDK的时间单位多以秒为单位,所以存储和计算时最好以秒为单位,而不是小时。如果需要显示或与您的通信时,您可以转换为小时服务器或其他任何东西。)

我假设您的用户界面中有一个按钮供用户在开始工作时点击。点击按钮时,记录当前时间,启动定时器定期更新显示,并立即更新显示:

    @IBOutlet var startWorkButton: UIButton!

    @IBAction func startWork() {
        guard secondsWorked == nil else {
            // Already started work.
            return
        }

        UserDefaults.standard.set(CFAbsoluteTimeGetCurrent(), forKey: workStartTimeKey)
        startDisplayUpdateTimer()
        updateViews()
    }

    private var displayUpdateTimer: Timer?

    private func startDisplayUpdateTimer() {
        displayUpdateTimer = Timer(timeInterval: 1, repeats: true, block: { [weak self] _ in
            self?.updateViews()
        })
        RunLoop.main.add(displayUpdateTimer!, forMode: .commonModes)
    }

我假设您还有另一个停止工作的按钮。当它被点击时,你抓取 secondsWorked 的当前值,清除存储的 start-work 时间,取消显示更新定时器,并用抓取的 secondsWorked 值做任何你想做的事情。在这里我只打印它:

    @IBOutlet var stopWorkButton: UIButton!

    @IBAction func stopWork() {
        guard let secondsWorked = self.secondsWorked else {
            // Didn't start work yet!
            return
        }

        displayUpdateTimer?.invalidate()
        displayUpdateTimer = nil
        UserDefaults.standard.removeObject(forKey: workStartTimeKey)
        updateViews()
        print("seconds worked: \(secondsWorked)")
    }

要更新显示,您可能希望在标签中显示当前工作时间。您还可以根据用户当前是否在工作来启用或禁用按钮:

    @IBOutlet var timeWorkedLabel: UILabel!

    private func updateViews() {
        if let secondsWorked = secondsWorked {
            startWorkButton.isEnabled = false
            stopWorkButton.isEnabled = true
            timeWorkedLabel.text = timeWorkedFormatter.string(from: secondsWorked)
        } else {
            startWorkButton.isEnabled = true
            stopWorkButton.isEnabled = false
            timeWorkedLabel.text = "Not working"
        }
    }

    private let timeWorkedFormatter: DateComponentsFormatter = {
        let formatter = DateComponentsFormatter()
        formatter.allowedUnits = [.hour, .minute, .second]
        formatter.allowsFractionalUnits = true
        formatter.collapsesLargestUnit = true
        formatter.maximumUnitCount = 2
        formatter.unitsStyle = .abbreviated
        formatter.zeroFormattingBehavior = .dropLeading
        return formatter
    }()

加载视图时,您应该查看用户是否已经在工作。如果应用程序在她开始工作后被终止并重新启动,就会发生这种情况。如果她已经在工作,请启动显示更新计时器。您还需要更新视图,无论如何:

    override func viewDidLoad() {
        super.viewDidLoad()
        if secondsWorked != nil { startDisplayUpdateTimer() }
        updateViews()
    }

最后,当视图控制器被销毁时,你应该取消定时器,以防你把它放在允许它被弹出的导航控制器中:

    deinit {
        displayUpdateTimer?.invalidate()
    }

}