NSTimer rapide en arrière-plan

J'ai rencontré beaucoup de problèmes avec comment gérer NSTimer en arrière-plan ici sur la stack ou ailleurs. J'ai essayé une de toutes les options qui ont vraiment du sens .. pour arrêter la timer lorsque l'application passe en arrière-plan avec

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

et

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

Au début je pensais que mon problème était résolu, je sauvegardais le moment où l'application entrait en arrière-plan et calculais la différence lorsque l'application entrait en premier plan .. mais plus tard j'ai remarqué que le time est effectivement différé de 3, 4, 5 secondes. que ce n'est pas la même chose .. Je l'ai comparé au chronomètre sur un autre appareil.

Y a-t-il VRAIMENT une solution SOLIDE pour exécuter un NSTimer en arrière-plan?

    Vous ne devriez pas jouer avec des ajustements basés sur le moment où il entre en arrière-plan ou reprend, mais plutôt simplement save le time que vous countz ou à (selon que vous countz vers le haut ou vers le bas). Ensuite, lorsque l'application redémarre, il vous suffit d'utiliser celle de / à l'heure lors de la reconstruction du minuteur.

    De même, assurez-vous que votre gestionnaire de timer ne dépend pas du moment exact où le sélecteur de gestion est appelé (par exemple, ne faites rien comme les seconds++ ou quoi que ce soit comme ça, mais revenez toujours en arrière) à cela de / à time.


    Voici un exemple de count à rebours, qui montre que nous ne "comptons" rien. Nous ne nous soucions pas non plus du time écoulé entre appDidEnterBackground et appDidBecomeActive . Sauvegardez simplement l'heure d'arrêt, puis le gestionnaire de timer compare simplement la date d' stopTime cible et l'heure actuelle, et affiche le time écoulé comme vous le souhaitez.

    Dans 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 sortinggger = UNTimeIntervalNotificationTrigger(timeInterval: stopTime.timeIntervalSinceNow, repeats: false) let notification = UNNotificationRequest(identifier: "timer", content: content, sortinggger: sortinggger) 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.ssortingng(from: now, to: stopTime!) } else { stopTimer() notifyTimerCompleted() } } private func notifyTimerCompleted() { timerLabel.text = "Timer done!" } } 

    Ou dans 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.ssortingngFromDate(now, toDate: stopTime!) } else { stopTimer() notifyTimerCompleted() } } func notifyTimerCompleted() { timerLabel.text = "Timer done!" } } 

    Par ailleurs, ce qui précède illustre également l'utilisation d'une notification locale (dans le cas où la timer expire alors que l'application n'est pas en cours d'exécution).

    Malheureusement, il n'existe aucun moyen fiable d'exécuter périodiquement certaines actions en arrière-plan. Vous pouvez utiliser des extractions d'arrière-plan, mais le operating system ne vous garantit pas que ceux-ci seront exécutés périodiquement.

    En arrière-plan, votre application est suspendue, et donc aucun code n'est exécuté, à l'exception des extractions d'arrière-plan mentionnées ci-dessus.