首页 > 解决方案 > 从后台计时器更新标签

问题描述

再会,

我正在开发一个运行良好的锻炼应用程序,除非将其移至后台。计时器暂停时暂停。我找到了一个我正在工作的后台计时器示例,但现在我无法让显示锻炼持续时间的 UILabel 工作。在控制台中,它声明我正在从我理解的主线程访问一个对象。我不知道该怎么做是让 UILabel 随着计时器从后台线程中更新而更新,更新标签位于主线程中。

这是我所拥有的(打印语句帮助我遵循代码):

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

    }
}

这是视图控制器的快照,我想更新 Duration。

快照

非常感谢任何人可以提供的任何帮助。

真挚地,

凯文

标签: iosswiftbackgrounduilabel

解决方案


与其尝试在后台运行计时器,不如记录startDate锻炼开始的时间并计算时间间隔。这样,应用程序实际上不必在后台运行来跟踪锻炼时间。计时器将仅用于更新用户界面。

现在暂停通过记录当前的锻炼间隔来工作。当锻炼重新开始时,它会从 中减去当前锻炼间隔Date()以获得新的调整后的startDate.

为进入后台和前台的应用添加通知,以便您可以在锻炼处于活动状态时重新启动 UI 更新计时器:

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()
    }
}

原始答案

只需调用updateTimerLabel()主循环:

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

全功能:

@objc func _backgroundTimerAction(_ timer: Timer) {

    print("_backgroundTimerAction(_:)")

    time += 1

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

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

笔记:

  1. 在后台线程上运行计时器并没有给你带来任何好处,只是设置它的麻烦。我建议只在主线程上运行它。
  2. 无需添加-> Void到 Swift 函数定义中;这是默认设置。
  3. 斯威夫特通常不需要分号;,所以丢掉那些。
  4. self.time已经是,因此没有必要从中Int创建新的。Int

    代替:

    let hours = Int(self.time) / 3600
    

    和:

    let hours = self.time / 3600
    

推荐阅读