обновить метку из фонового таймера - PullRequest
0 голосов
/ 06 мая 2018

Добрый день,

Я занимаюсь разработкой приложения для тренировок, которое работает нормально, за исключением перемещения его в фоновый режим. Таймер приостанавливается, когда это происходит. Я нашел пример фонового таймера, который у меня работает, но теперь я не могу получить 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

    }
}

Вот снимок контроллера представления, и я хочу обновить Длительность.

snapshot

Любая помощь, которую кто-либо может оказать, высоко ценится.

С уважением,

Кевин

1 Ответ

0 голосов
/ 06 мая 2018

Вместо того, чтобы пытаться запустить таймер в фоновом режиме, запишите startDate начала тренировки и вычислите временной интервал. Таким образом, приложение не должно запускаться в фоновом режиме, чтобы отслеживать время тренировки. Таймер будет использоваться только для обновления пользовательского интерфейса.

Пауза теперь работает, записывая текущий интервал тренировки. Когда тренировка возобновляется, она вычитает текущий интервал тренировки из Date(), чтобы получить новый скорректированный startDate.

Добавьте уведомления для приложения, вводящего фон и передний план, чтобы можно было перезапустить таймер обновления интерфейса пользователя, если тренировка активна:

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
    
...