2 votes

Comment éviter la dérive sur une minuterie de fond courte ?

J'ai une minuterie 90s qui s'exécute en arrière-plan jusqu'à la fin de l'enregistrement. beginBackgroundTaskWithExpirationHandler . Cela semble simple, mais le problème que je rencontre est que la minuterie elle-même dérive considérablement. Pour le minuteur des années 90, j'obtiens environ 30 à 35 secondes de dérive. En d'autres termes, si je lance le minuteur, que je mets l'application en arrière-plan, puis que j'ouvre l'application après 90 secondes, le minuteur affiche 30 secondes restantes.

Si je laisse l'application ouverte pendant toute la durée des années 90, je n'obtiens aucune dérive. Si je réduis le timeInterval du timer à 1s (au lieu de mon préféré 0,05s), la dérive du fond disparaît.

Que puis-je faire pour éliminer la dérive de l'arrière-plan sans perdre la précision de la minuterie ?

class TimerViewController: UIViewController {

    @IBOutlet weak var startTimerButton: UIButton!
    @IBOutlet weak var timerLabel: UILabel!
    @IBOutlet weak var resetTimerButton: UIButton!

    var timer = NSTimer()
    let timeInterval:NSTimeInterval = 0.05
    let timerEnd:NSTimeInterval = 90
    var timeCount:NSTimeInterval = 0

    var backgroundTask: UIBackgroundTaskIdentifier = UIBackgroundTaskInvalid
    var backgroundTaskIdentifier: UIBackgroundTaskIdentifier?

    override func viewDidLoad() {
        super.viewDidLoad()

        resetTimeCount()
        timerLabel.text = timeString(timeCount)

        NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(TimerViewController.reinstateBackgroundTask), name: UIApplicationDidBecomeActiveNotification, object: nil)
    }

    deinit {
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    // MARK: Actions

    @IBAction func startTimerButtonTapped(sender: UIButton) {

        if !timer.valid { //prevent more than one timer on the thread
            timerLabel.text = timeString(timeCount)
            timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval, target: self,selector: #selector(TimerViewController.timerDidEnd(_:)),userInfo: nil, repeats: true)
            schedulePushNotification()
            registerBackgroundTask()
        }

    }

    @IBAction func resetTimerButtonTapped(sender: UIButton) {
        timer.invalidate()
        resetTimeCount()
        timerLabel.text = timeString(timeCount)
        cancelAllNotifications()
    }

    // MARK: Timer

    func resetTimeCount(){
        timeCount = timerEnd
    }

    func timerDidEnd(timer: NSTimer){
        //timer that counts down
        timeCount = timeCount - timeInterval
        if timeCount <= 0 {  //test for target time reached.
            timerLabel.text = "Time is up!!"
            timer.invalidate()
            AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
            if backgroundTask != UIBackgroundTaskInvalid {
                endBackgroundTask()
                cancelAllNotifications()
            }
        } else { //update the time on the clock if not reached
            timerLabel.text = timeString(timeCount)
        }

    }

    func timeString(time: NSTimeInterval) -> String {
        let minutes = Int(time) / 60
        let seconds = time - Double(minutes) * 60
        let secondsFraction = seconds - Double(Int(seconds))
        return String(format:"%02i:%02i.%02i",minutes,Int(seconds),Int(secondsFraction * 100.0))
    }

    // MARK: BackgroundTask

    func registerBackgroundTask() {
        backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler {
            [unowned self] in
            self.endBackgroundTask()
        }
        assert(backgroundTask != UIBackgroundTaskInvalid)
    }

    func reinstateBackgroundTask() {
        if timeCount != 0.0 && (backgroundTask == UIBackgroundTaskInvalid) {
            registerBackgroundTask()
        }
    }

    func endBackgroundTask() {
        UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
        backgroundTask = UIBackgroundTaskInvalid
    }

    // MARK: Notifications

    func schedulePushNotification() {
        let notification = UILocalNotification()
        notification.alertAction = "Go back to App"
        notification.alertBody = "The 90s timer is finished!"
        notification.fireDate = NSDate(timeIntervalSinceNow: timerEnd+1)
        UIApplication.sharedApplication().scheduleLocalNotification(notification)

    }

    func cancelAllNotifications() {
        UIApplication.sharedApplication().cancelAllLocalNotifications()
    }

}

1voto

briancl Points 71

J'ai laissé tomber la tâche d'arrière-plan et l'ai remplacée par le stockage en NSUserDefaults . La dérive a disparu. Voici le code final :

class TimerViewController: UIViewController {

@IBOutlet weak var startTimerButton: UIButton!
@IBOutlet weak var timerLabel: UILabel!
@IBOutlet weak var resetTimerButton: UIButton!

var timer = NSTimer()
let timeInterval:NSTimeInterval = 0.01
let timerEnd:NSTimeInterval = 90
var timeCount:NSTimeInterval = 0

override func viewDidLoad() {
    super.viewDidLoad()

    resetTimeCount()
    timerLabel.text = timeString(timeCount)

    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(TimerViewController.applicationWillResignActive),name: UIApplicationWillResignActiveNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(TimerViewController.applicationDidBecomeActive),name: UIApplicationDidBecomeActiveNotification, object: nil)
}

deinit {
    NSNotificationCenter.defaultCenter().removeObserver(self)
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: Actions

@IBAction func startTimerButtonTapped(sender: UIButton) {

    if !timer.valid { //prevent more than one timer on the thread
        timerLabel.text = timeString(timeCount)
        timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval, target: self,selector: #selector(TimerViewController.timerDidEnd(_:)),userInfo: nil, repeats: true)
        schedulePushNotification()
    }

}

@IBAction func resetTimerButtonTapped(sender: UIButton) {
    timer.invalidate()
    resetTimeCount()
    timerLabel.text = timeString(timeCount)
    cancelAllNotifications()
}

// MARK: Timer

func resetTimeCount(){
    timeCount = timerEnd
}

func timerDidEnd(timer: NSTimer){
    //timer that counts down
    timeCount = timeCount - timeInterval
    if timeCount <= 0 {  //test for target time reached.
        timerLabel.text = "Time is up!!"
        timer.invalidate()
        AudioServicesPlayAlertSound(kSystemSoundID_Vibrate)
        cancelAllNotifications()
    } else { //update the time on the clock if not reached
        timerLabel.text = timeString(timeCount)
    }
}

func timeString(time: NSTimeInterval) -> String {
    let minutes = Int(time) / 60
    let seconds = time - Double(minutes) * 60
    let secondsFraction = seconds - Double(Int(seconds))
    return String(format:"%02i:%02i.%02i",minutes,Int(seconds),Int(secondsFraction * 100.0))
}

// MARK: Timer Storage

struct PropertyKey {
    static let timeCountKey = "TimerViewController_timeCount"
    static let timeMeasurementKey = "TimerViewController_timeMeasurement"
}

dynamic private func applicationWillResignActive() {
    if !timer.valid {
        clearDefaults()
        NSLog("Clearning defaults")
    } else {
        saveDefaults()
        NSLog("Saving defaults")
    }
}

dynamic private func applicationDidBecomeActive() {
    if timer.valid {
        loadDefaults()
        NSLog("Loading defaults")
    }
}

private func saveDefaults() {
    let userDefault = NSUserDefaults.standardUserDefaults()
    userDefault.setObject(timeCount, forKey: PropertyKey.timeCountKey)
    userDefault.setObject(NSDate().timeIntervalSince1970, forKey: PropertyKey.timeMeasurementKey)

    userDefault.synchronize()

}

private func clearDefaults() {
    let userDefault = NSUserDefaults.standardUserDefaults()
    userDefault.removeObjectForKey(PropertyKey.timeCountKey)
    userDefault.removeObjectForKey(PropertyKey.timeMeasurementKey)

    userDefault.synchronize()

}

private func loadDefaults() {
    let userDefault = NSUserDefaults.standardUserDefaults()
    let restoredTimeCount = userDefault.objectForKey(PropertyKey.timeCountKey) as! Double
    let restoredTimeMeasurement = userDefault.objectForKey(PropertyKey.timeMeasurementKey) as! Double

    let timeDelta = NSDate().timeIntervalSince1970 - restoredTimeMeasurement
    print(timeDelta)
    print(timeCount - restoredTimeCount - timeDelta)
    timeCount = restoredTimeCount - timeDelta
}

// MARK: Notifications

func schedulePushNotification() {
    let notification = UILocalNotification()
    notification.alertAction = "Go back to App"
    notification.alertBody = "The 90s timer is finished!"
    notification.fireDate = NSDate(timeIntervalSinceNow: timerEnd+1)
    UIApplication.sharedApplication().scheduleLocalNotification(notification)

}

func cancelAllNotifications() {
    UIApplication.sharedApplication().cancelAllLocalNotifications()
}

}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X