我在堆栈或其他地方在后台处理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真的有任何SOLID解决方案吗?
您不应该根据进入后台或恢复的时间来进行任何调整,而只需节省您要数的时间(取决于您是在递增还是递减)。然后,当应用再次启动时,您只需在重构计时器时使用该时间即可。
同样,请确保您的计时器处理程序不依赖于调用处理选择器的确切时间(例如, 不要 执行seconds++任何类似操作,因为您可能希望调用时不会准确地调用它),但请务必返回从/到时间。
seconds++
这是一个倒数计时器的示例,它说明我们不对任何东西进行“计数”。我们也不在乎介于appDidEnterBackground和之间的时间appDidBecomeActive。只需保存停止时间,然后计时器处理程序就可以将目标stopTime时间与当前时间进行比较,并根据需要显示经过的时间。
appDidEnterBackground
appDidBecomeActive
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!" } }
顺便说一句,以上内容还说明了本地通知的使用(以防定时器在应用当前未运行时到期)。