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.
Any assistance anyone can provide is greatly appreciated.
Sincerely,
Kevin
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 adjustedstartDate
.Add notifications for the app entering the background and foreground so that you can restart the UI update timer if the workout is active:
Original Answer
Just call
updateTimerLabel()
on the main loop:Full function:
Notes:
-> Void
to a Swift function definition; that is the default.;
, so lose those.self.time
is already anInt
, so creating a newInt
from it is unnecessary.replace:
with: