快速 NSTimer 在后台

Posted

技术标签:

【中文标题】快速 NSTimer 在后台【英文标题】:swift NSTimer in Background 【发布时间】:2016-04-02 11:19:52 【问题描述】:

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

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

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

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

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

【问题讨论】:

【参考方案1】:

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

在后台时,您的应用程序被挂起,因此除了上述后台提取之外,不会执行任何代码。

【讨论】:

在后台安排本地通知怎么样?我想将本地通知的 fireDate 与重复的计时器的 fireDate 同步..所以如果我提前安排所有通知..并且用户在会话中间打开应用程序..计时器时间被推迟并且通知时间会随着计时器的结束而变化 本地通知不携带代码执行,除非用户选择本地通知报告的动作之一。 但是本地通知正是应用程序如何通知用户计时器已过期,无论应用程序是否正在运行。是的,用户决定是否应该重新启动应用程序并采取行动,但您肯定希望将本地通知用于倒计时应用程序。【参考方案2】:

您不应该根据它进入后台或恢复的时间来进行任何调整,而只是节省您从或到的时间(取决于您是向上还是向下计数)。然后,当应用程序再次启动时,您只需在重建计时器时使用该从/到时间。

同样,请确保您的计时器处理程序不依赖于调用处理选择器的确切时间(例如, 做任何类似seconds++ 或类似的事情,因为它可能不会被调用正是你希望的时候),但总是回到那个从/到那个时间。


这是一个倒计时的例子,它说明我们不“计数”任何东西。我们也不关心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。

【讨论】:

我有一个可以计算秒数的计时器,我将这些秒数从某个固定时间中减去,然后将其与 0 进行比较以了解计时器是否已完成。我还应该如何从计时器中分出? 计时器不应该“计算”任何东西。计时器应获取当前时间(例如,来自 CFAbsoluteTimeGetCurrent()CACurrentMediaTime()[NSDate date])并将其与您倒数到的基线时间进行比较,以便了解剩余时间。 @kalafun 好的,关于那个单独的计时器,现实情况是,如果应用程序没有运行,您无法启动另一个计时器,直到用户再次启动应用程序(通过点击通知或恰好重新启动应用程序)。所以,你有两个选择。要么预先创建两个计时器(在用户离开应用程序之前),要么在用户重新启动应用程序时创建第二个计时器。使用后一种方法,您必须根据保存在持久存储中的时间计算第二个计时器的详细信息。 @kalafun - 这取决于您是否可以预先创建两个计时器/通知,或者第二个计时器是否以某种方式依赖于您只有在第一个计时器完成时才知道的东西。但是,如果我事先知道我想要第二个计时器,它会在第一个计时器之后触发 x 分钟(例如一些“打盹”警报),我个人倾向于创建两个本地通知前面(如果应用程序响应第一个计时器重新启动,则取消第二个)。 @Oleksandr - 您显然传递的参数不是Date 对象。以上使用Date

以上是关于快速 NSTimer 在后台的主要内容,如果未能解决你的问题,请参考以下文章

在后台运行 NSTimer 方法

如何在后台使用 nstimer

当应用程序后台运行时,NSTimer 是不是会触发?

如何让我的应用在后台运行 NSTimer?

我们如何让 NSTimer 在 iOS 中为我​​的音频播放器在后台运行

NSTimer 在后台模式下使用 locationManager 委托(“作弊方式”)SWIFT