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 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:
- 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.
- There is no need to add
-> Void
to a Swift function definition; that is the default.
- Swift typically doesn't need the semicolons
;
, so lose those.
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