Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
175 views
in Technique[技术] by (71.8m points)

ios - update label from background timer

Good Day,

I am developing a workout app that works fine except when moving it to the background. The timer suspends when it does. I found an example of a background timer that I have working but now I can't get the UILabel that displays the duration of the workout to work. In the console it states that I am accessing an object from the main thread which I understand. What I don't know how to do is to get the UILabel to update as the timer updates from within the background thread with the update label being in the main thread.

Here is what I have (print statements help me follow the code):

import UIKit

class ViewController: UIViewController {

    var time = 0

    var timer = Timer()

    @IBOutlet weak var outputLabel: UILabel!

    @IBOutlet weak var start: UIButton!

    @IBOutlet weak var paused: UIButton!

    @IBAction func startButton(_ sender: UIButton) {

        startButtonPressed()

    }

    @IBAction func pausedButton(_ sender: UIButton) {

        pausedButtonPressed()

    }

    @IBOutlet weak var timerLabel: UILabel!

    func updateTimerLabel() {

        let hours = Int(self.time) / 3600
        let minutes = Int(self.time) / 60 % 60
        let seconds = Int(self.time) % 60

        timerLabel.text = String(format:"%02i:%02i:%02i", hours, minutes, seconds)

    }

    func startButtonPressed() {

        outputLabel.text = "Workout Started"
        start.isHidden = true
        paused.isHidden = false

        _backgroundTimer(repeated: true)
        print("Calling _backgroundTimer(_:)")

    }

    func pausedButtonPressed(){

        outputLabel.text = "Workout Paused"
        timer.invalidate()
        pauseWorkout()

    }

    func pauseWorkout(){

        paused.isHidden = true
        start.isHidden = false

    }


    func _backgroundTimer(repeated: Bool) -> Void {
        NSLog("_backgroundTimer invoked.");

        //The thread I used is a background thread, dispatch_async will set up a background thread to execute the code in the block.

        DispatchQueue.global(qos:.userInitiated).async{
            NSLog("NSTimer will be scheduled...");

            //Define a NSTimer
            self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self._backgroundTimerAction(_:)), userInfo: nil, repeats: true);
            print("Starting timer")

            //Get the current RunLoop
            let runLoop:RunLoop = RunLoop.current;

            //Add the timer to the RunLoop
            runLoop.add(self.timer, forMode: RunLoopMode.defaultRunLoopMode);

            //Invoke the run method of RunLoop manually
            NSLog("NSTimer scheduled...");
            runLoop.run();

        }

    }

    @objc func _backgroundTimerAction(_ timer: Foundation.Timer) -> Void {

        print("_backgroundTimerAction(_:)")

        time += 1

        NSLog("time count -> (time)");
    }


    override func viewDidLoad() {
        super.viewDidLoad()

        print("viewDidLoad()")

        print("Hiding buttons")
        paused.isHidden = true
        start.isHidden = false

        print("Clearing Labels")
        outputLabel.text = ""
        timerLabel.text = ""

        print("(timer)")
        timer.invalidate()
        time = 0

    }
}

Here is a snapshot of the view controller and I want to update the Duration.

snapshot

Any assistance anyone can provide is greatly appreciated.

Sincerely,

Kevin

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Instead of trying to run a timer in the background, record the startDate of the start of your workout and compute the time interval. That way, the app doesn't actually have to run in the background to keep track of the workout time. The timer will only be used to update the user interface.

Pausing now works by recording the current workout interval. When the workout restarts, it subtracts the current workout interval from the Date() to get a new adjusted startDate.

Add notifications for the app entering the background and foreground so that you can restart the UI update timer if the workout is active:

import UIKit

enum WorkoutState {
    case inactive
    case active
    case paused
}

class ViewController: UIViewController {

    var workoutState = WorkoutState.inactive
    var workoutInterval = 0.0
    var startDate = Date()

    var timer = Timer()

    @IBOutlet weak var outputLabel: UILabel!

    @IBOutlet weak var start: UIButton!

    @IBOutlet weak var paused: UIButton!

    @IBAction func startButton(_ sender: UIButton) {

        startButtonPressed()

    }

    @IBAction func pausedButton(_ sender: UIButton) {

        pausedButtonPressed()

    }

    @IBOutlet weak var timerLabel: UILabel!

    func updateTimerLabel() {
        let interval = -Int(startDate.timeIntervalSinceNow)
        let hours = interval / 3600
        let minutes = interval / 60 % 60
        let seconds = interval % 60

        timerLabel.text = String(format:"%02i:%02i:%02i", hours, minutes, seconds)

    }

    func startButtonPressed() {

        if workoutState == .inactive {
            startDate = Date()
        } else if workoutState == .paused {
            startDate = Date().addingTimeInterval(-workoutInterval)
        }
        workoutState = .active

        outputLabel.text = "Workout Started"
        start.isHidden = true
        paused.isHidden = false

        updateTimerLabel()
        _foregroundTimer(repeated: true)
        print("Calling _foregroundTimer(_:)")

    }

    func pausedButtonPressed(){

        // record workout duration
        workoutInterval = floor(-startDate.timeIntervalSinceNow)

        outputLabel.text = "Workout Paused"
        workoutState = .paused
        timer.invalidate()
        pauseWorkout()

    }

    func pauseWorkout(){

        paused.isHidden = true
        start.isHidden = false

    }

    func _foregroundTimer(repeated: Bool) -> Void {
        NSLog("_foregroundTimer invoked.");

        //Define a Timer
        self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerAction(_:)), userInfo: nil, repeats: true);
        print("Starting timer")

    }

    @objc func timerAction(_ timer: Timer) {

        print("timerAction(_:)")

        self.updateTimerLabel()
    }

    @objc func observerMethod(notification: NSNotification) {

        if notification.name == .UIApplicationDidEnterBackground {
            print("app entering background")

            // stop UI update
            timer.invalidate()
        } else if notification.name == .UIApplicationDidBecomeActive {
            print("app entering foreground")

            if workoutState == .active {
                updateTimerLabel()
                _foregroundTimer(repeated: true)
            }
        }

    }

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(self, selector: #selector(observerMethod), name: .UIApplicationDidEnterBackground, object: nil)

        NotificationCenter.default.addObserver(self, selector: #selector(observerMethod), name: .UIApplicationDidBecomeActive, object: nil)

        print("viewDidLoad()")

        print("Hiding buttons")
        paused.isHidden = true
        start.isHidden = false

        print("Clearing Labels")
        outputLabel.text = ""
        timerLabel.text = ""

        print("(timer)")
        timer.invalidate()
    }
}

Original Answer

Just call updateTimerLabel() on the main loop:

DispatchQueue.main.async {
    self.updateTimerLabel()
}

Full function:

@objc func _backgroundTimerAction(_ timer: Timer) {

    print("_backgroundTimerAction(_:)")

    time += 1

    DispatchQueue.main.async {
        self.updateTimerLabel()
    }

    NSLog("time count -> (time)")
}

Notes:

  1. Running the timer on a background thread isn't buying you anything but trouble in setting it up. I'd recommend just running it on the main thread.
  2. There is no need to add -> Void to a Swift function definition; that is the default.
  3. Swift typically doesn't need the semicolons ;, so lose those.
  4. self.time is already an Int, so creating a new Int from it is unnecessary.

    replace:

    let hours = Int(self.time) / 3600
    

    with:

    let hours = self.time / 3600
    

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...