Running a Timer that counts down in background whe

2019-09-11 14:12发布

问题:

I want my countdown timer to suspend and then resume when the app leaves / returns to focus, using the time away to calculate how much time should be deducted.

I am using the app delegate file (not sure thats the right location? or if they are meant to be in the view controllers file as functions of their own?)

Issue is im getting a lot of errors such as:

Value of type 'AppDelegate' has no member 'restTimer'

Use of unresolved identifier 'nextFireDate'

Use of unresolved identifier 'selector'

restTimer was declared as a timer in my view controllers file but when i tried these blocks in that file i got an equal number of errors for unresolved identifiers

and using the following 2 code blocks

    func applicationWillResignActive(_ application: UIApplication) {
        guard let t = self.restTimer else { return }
        nextFireDate = t.fireDate
        t.invalidate()

and

func applicationDidBecomeActive(_ application: UIApplication) {
        guard let n = nextFireDate else { return }
        let howMuchLonger = n.timeIntervalSinceDate(NSDate())
        if howMuchLonger < 0 {
            print("Should have already fired \(howMuchLonger) seconds ago")
            target!.performSelector(selector!)
        } else {
            print("should fire in \(howMuchLonger) seconds")
            Timer.scheduledTimerWithTimeInterval(howMuchLonger, target: target!, selector: selector!, userInfo: nil, repeats: false)
        }
}

UPDATE: Added full views code due to issue incorporating the answer

import Foundation
import UIKit

class RestController: UIViewController {

    @IBOutlet weak var restRemainingCountdownLabel: UILabel!
    @IBOutlet weak var setsRemainingCountdownLabel: UILabel!
    @IBOutlet weak var numberOfSetsLabel: UILabel!
    @IBOutlet weak var numberOfRestLabel: UILabel!
    @IBOutlet weak var adjustSetsStepper: UIStepper!
    @IBOutlet weak var adjustRestStepper: UIStepper!

    var startDate: Date!
    let startDateKey = "start.date"
    let interval = TimeInterval(20)

    var restTimer: Timer!
    var restCount = 0
    var setCount = 0
    var selectedTime = 1
    var selectedSets = 1

    private let resignDateKey = "resign.date"

    @IBAction func endSetPressed(_ sender: Any) {
        if (setCount > 0){
            setCount -= 1
            setsRemainingCountdownLabel.text = String(setCount)
        }
        handleTimer()
    }

    @IBAction func setStepperValueChanged(_ sender: UIStepper) {
        numberOfSetsLabel.text = Int(sender.value).description
        self.setCount = Int(sender.value)
        self.selectedSets = setCount
        setsRemainingCountdownLabel.text = String(setCount)
    }

    @IBAction func restStepperValueChanged(_ sender: UIStepper) {
        numberOfRestLabel.text = Int(sender.value).description
        let timeMinSec = timeFormatted(totalSeconds: Int(sender.value)*60)
        restRemainingCountdownLabel.text = timeMinSec
        self.selectedTime = Int(sender.value)
        restCount = self.selectedTime * 60
    }

    @IBAction func resetSetsButton(_ sender: Any) {
        setCount = Int(adjustSetsStepper.value)
        setsRemainingCountdownLabel.text = String(setCount)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        numberOfSetsLabel.text = String(selectedSets)
        numberOfRestLabel.text = String(selectedTime)

        createTimer(interval: interval)
        NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @objc private func willResignActive(notification: Notification) {
        print("resigning")
        guard restTimer.isValid else {
            UserDefaults.standard.removeObject(forKey: startDateKey)
            return
        }
        restTimer.invalidate()
        UserDefaults.standard.set(Date(), forKey: startDateKey)
    }

    @objc private func didBecomeActive(notification: Notification) {
        print("resume")
        if let startDate = UserDefaults.standard.object(forKey: startDateKey) as? Date {
            let elapsed = -startDate.timeIntervalSinceNow
            print("elpased time: \(elapsed) remaining time: \(interval - elapsed)")
            if elapsed > interval {
                timerUp()
            } else {
                createTimer(interval: interval - elapsed)
            }
        }
    }

    private func createTimer (interval: TimeInterval) {
        restTimer = Timer.scheduledTimer(withTimeInterval: interval , repeats: false) {[weak self] _ in
            self?.timerUp()
        }
        startDate = Date()
    }

    private func timerUp() {
        print("At least \(interval) seconds has elapsed")
    }


    func handleSets() {

        if (setCount > 0) {

            self.restCount = self.selectedTime * 60
        }
        handleTimer()
    }

    func handleTimer() {

        if (restTimer?.isValid ?? false) {

            restTimer?.invalidate()
            restTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(RestController.updateTimer), userInfo: nil, repeats: true)

        } else {

            restTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(RestController.updateTimer), userInfo: nil, repeats: true)
        }
    }

    func updateTimer() {
        if (restCount > 0){
            restCount -= 1
        } else if (restCount == 0){
            restTimer?.invalidate()

        }
        restRemainingCountdownLabel.text = timeFormatted(totalSeconds: restCount)
    }

    func timeFormatted(totalSeconds: Int) -> String {
        let seconds: Int = totalSeconds % 60
        let minutes: Int = (totalSeconds / 60) % 60
        return String(format: "%02d:%02d", minutes, seconds)
    }

回答1:

I think you cannot rely on the fact that the app will remain in memory while it is in the background. Thus, you should archive all the data you need to recreate the timer at the exact point.

For example, in applicationWillResignActive

UserDefaults.standard.set(value: t.nextFireDate forKey:"NextFireDate")

and in applicationWillEnterForeground

if let fireDate = UserDefaults.standard.object(forKey: "NextFireDate") {
    // setup a timer with the correct fire date
}


回答2:

You don't have to use the AppDelegate for this because it also posts notifications. You can use the AppDelegate if you want. Here is code using notifications:

    class ViewController: UIViewController{
        private let startDateKey = "start.date"
        private let interval = TimeInterval(20)
        private var startDate: Date!
        private var timer: Timer!
        override func viewDidLoad() {
            super.viewDidLoad()
            createTimer(interval: interval)
            NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: NSNotification.Name.UIApplicationWillResignActive, object: nil)
            NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationDidBecomeActive, object: nil)
        }

        deinit {
            NotificationCenter.default.removeObserver(self)
        }

        @objc private func willResignActive(notification: Notification) {
            print("resigning")
            guard timer.isValid else {
                UserDefaults.standard.removeObject(forKey: startDateKey)
                return
            }
            timer.invalidate()
            UserDefaults.standard.set(Date(), forKey: startDateKey)
        }

        @objc private func didBecomeActive(notification: Notification) {
            print("resume")
            if let startDate = UserDefaults.standard.object(forKey: startDateKey) as? Date {
                let elapsed = -startDate.timeIntervalSinceNow
                print("elpased time: \(elapsed) remaining time: \(interval - elapsed)")
                if elapsed > interval {
                    timerUp()
                } else {
                    createTimer(interval: interval - elapsed)
                }
            }
        }

        private func createTimer (interval: TimeInterval) {
            timer = Timer.scheduledTimer(withTimeInterval: interval , repeats: false) {[weak self] _ in
                self?.timerUp()
            }
            startDate = Date()
        }

        private func timerUp() {
            print("At least \(interval) seconds has elapsed")
        }

    }