迅速的NSTimer在背景中

我遇到了很多关于如何在后台处理NSTimer的问题。 我已经尝试了所有的选项之一,实际上是有道理的..当应用程序去背景停止计时器

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

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

起初我以为我的问题解决了,我只是节省了时间,当应用程序进入背景,并计算出应用程序进入前台时的差异。但后来我注意到,时间实际上是推迟了3,4,5秒。 。它实际上是不一样的。我已经比较了另一台设备上的秒表。

有没有真正的任何SOLID解决scheme在后台运行NSTimer?

你不应该根据什么时候进入后台或者恢复时间来调整,而只需要保存你正在计数的时间或者取决于你正在计数的时间。 然后,当应用程序再次启动,你只是用来重build计时器的时间。

同样,确保你的定时器处理程序不依赖于处理select器被调用的确切时间(例如, 不要做任何事情,像seconds++或类似的东西,因为它可能不会被精确调用,当你希望它会),但总是回来到/从时间。


这是一个倒数计时器的例子,它说明我们不“计数”任何东西。 我们也不关心appDidEnterBackgroundappDidBecomeActive之间的时间appDidBecomeActive 。 只需保存停止时间,然后定时器处理程序只是比较目标停止时间和当前时间,然后显示所需的时间。

在Swift 3:

 import UIKit import UserNotifications private let stopTimeKey = "stopTimeKey" class ViewController: UIViewController { @IBOutlet weak var datePicker: UIDatePicker! @IBOutlet weak var timerLabel: UILabel! private var stopTime: Date? 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() } } } private func registerForLocalNotifications() { if #available(iOS 10, *) { UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, error in guard granted && error == nil else { // display error print("\(error)") return } } } else { let types: UIUserNotificationType = [.badge, .sound, .alert] let settings = UIUserNotificationSettings(types: types, categories: nil) UIApplication.shared.registerUserNotificationSettings(settings) } } @IBAction func didTapStartButton(_ sender: AnyObject) { let time = datePicker.date if time > Date() { startTimer(time) } else { timerLabel.text = "timer date must be in future" } } // MARK: Timer stuff private var timer: Timer? private 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) } } private func stopTimer() { timer?.invalidate() timer = nil } private let dateComponentsFormatter: DateComponentsFormatter = { let _formatter = DateComponentsFormatter() _formatter.allowedUnits = [.hour, .minute, .second] _formatter.unitsStyle = .positional _formatter.zeroFormattingBehavior = .pad return _formatter }() // 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 `NSDateComponentsFormatter`, I'd suggest considering // `NSCalendar` method `components:fromDate:toDate:options:` to // get the number of hours, minutes, seconds, etc. between two // dates. func handleTimer(_ timer: Timer) { let now = Date() if stopTime! > now { timerLabel.text = dateComponentsFormatter.string(from: now, to: stopTime!) } else { stopTimer() notifyTimerCompleted() } } private func notifyTimerCompleted() { timerLabel.text = "Timer done!" } } 

或者在Swift 2中:

 private let stopTimeKey = "stopTimeKey" class ViewController: UIViewController { @IBOutlet weak var datePicker: UIDatePicker! @IBOutlet weak var timerLabel: UILabel! var stopTime: NSDate? override func viewDidLoad() { super.viewDidLoad() registerForLocalNotifications() stopTime = NSUserDefaults.standardUserDefaults().objectForKey(stopTimeKey) as? NSDate if let time = stopTime { if time.compare(NSDate()) == .OrderedDescending { startTimer(time) } else { notifyTimerCompleted() } } } func registerForLocalNotifications() { let types: UIUserNotificationType = [.Badge, .Sound, .Alert] let settings = UIUserNotificationSettings(forTypes: types, categories: nil) UIApplication.sharedApplication().registerUserNotificationSettings(settings) } @IBAction func didTapStartButton(sender: AnyObject) { let time = datePicker.date if time.compare(NSDate()) == .OrderedDescending { startTimer(time) } else { timerLabel.text = "timer date must be in future" } } // MARK: Timer stuff var timer: NSTimer? func startTimer(stopTime: NSDate) { // save `stopTime` in case app is terminated NSUserDefaults.standardUserDefaults().setObject(stopTime, forKey: stopTimeKey) self.stopTime = stopTime // start NSTimer timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "handleTimer:", userInfo: nil, repeats: true) // start local notification (so we're notified if timer expires while app is not running) let notification = UILocalNotification() notification.fireDate = stopTime notification.alertBody = "Timer finished!" UIApplication.sharedApplication().scheduleLocalNotification(notification) } func stopTimer() { timer?.invalidate() timer = nil } let dateComponentsFormatter: NSDateComponentsFormatter = { let _formatter = NSDateComponentsFormatter() _formatter.allowedUnits = [.Hour, .Minute, .Second] _formatter.unitsStyle = .Positional _formatter.zeroFormattingBehavior = .Pad return _formatter }() // I'm going to use `NSDateComponentsFormatter` 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 `NSDateComponentsFormatter`, I'd suggest considering // `NSCalendar` method `components:fromDate:toDate:options:` to // get the number of hours, minutes, seconds, etc. between two // dates. func handleTimer(timer: NSTimer) { let now = NSDate() if stopTime!.compare(now) == .OrderedDescending { timerLabel.text = dateComponentsFormatter.stringFromDate(now, toDate: stopTime!) } else { stopTimer() notifyTimerCompleted() } } func notifyTimerCompleted() { timerLabel.text = "Timer done!" } } 

顺便说一下,上面还说明了使用本地通知(如果定时器在应用程序当前不在运行时到期)。

不幸的是,没有可靠的方法来定期在后台运行某些操作。 您可以使用后台提取,但操作系统并不能保证这些将被定期执行。

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