I'm building an app and I need a timer to run if the user sends the screen to the background, or if they put the phone in sleep and open it again. I need the timer to still be going.
I tried recording the time when I exit the and enter it again, subtracting the two and adding that to the running count, and it seems to work fine on the Xcode simulator but when I run it on my phone it doesn't work. Any ideas?
Here is the code for reference.
And the timer starts with a button I didn't include that part but it's just a simple IBAction that calls the timer.fire() function.
var time = 0.0
var timer = Timer()
var exitTime : Double = 0
var resumeTime : Double = 0
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
exitTime = Date().timeIntervalSinceNow
}
override func awakeFromNib() {
super.awakeFromNib()
resumeTime = Date().timeIntervalSinceNow
time += (resumeTime-exitTime)
timer.fire()
}
func startTimer() {
if !isTimeRunning {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(WorkoutStartedViewController.action), userInfo: nil, repeats: true)
isTimeRunning = true
}
}
func pauseTimer() {
timer.invalidate()
isTimeRunning = false
}
#objc func action()
{
time += 0.1
timerLabel.text = String(time)
let floorCounter = Int(floor(time))
let hour = floorCounter/3600
let minute = (floorCounter % 3600)/60
var minuteString = "\(minute)"
if minute < 10 {
minuteString = "0\(minute)"
}
let second = (floorCounter % 3600) % 60
var secondString = "\(second)"
if second < 10 {
secondString = "0\(second)"
}
if time < 3600.0 {
timerLabel.text = "\(minuteString):\(secondString)"
} else {
timerLabel.text = "\(hour):\(minuteString):\(secondString)"
}
}
You do have the right idea but the first problem I see is that viewWillDissapear is only called when you leave a view controller to go to a new viewController - It is not called when the app leaves the view to enter background (home button press)
I believe the callback functions you are looking for are UIApplication.willResignActive (going to background) and UIApplication.didBecomeActive (app re-opened)
You can access these methods in the AppDelegate or you can set them up on a view controller heres a mix of your code and some changes to produce a working sample on one initial VC:
import UIKit
import CoreData
class ViewController: UIViewController {
#IBOutlet weak var timerLabel: UILabel!
var time = 0.0
var timer = Timer()
var exitTime : Date? // Change to Date
var resumeTime : Date? // Change to Date
var isTimeRunning = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
startTimer()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
NotificationCenter.default.addObserver(self,
selector: #selector(applicationDidBecomeActive),
name: UIApplication.didBecomeActiveNotification,
object: nil)
// Add willResign observer
NotificationCenter.default.addObserver(self,
selector: #selector(applicationWillResign),
name: UIApplication.willResignActiveNotification,
object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
// Remove becomeActive observer
NotificationCenter.default.removeObserver(self,
name: UIApplication.didBecomeActiveNotification,
object: nil)
// Remove becomeActive observer
NotificationCenter.default.removeObserver(self,
name: UIApplication.willResignActiveNotification,
object: nil)
}
func startTimer() {
if !isTimeRunning {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:
#selector(self.action), userInfo: nil, repeats: true)
isTimeRunning = true
}
}
#objc func action() {
time += 0.1
timerLabel.text = String(time)
let floorCounter = Int(floor(time))
let hour = floorCounter/3600
let minute = (floorCounter % 3600)/60
var minuteString = "\(minute)"
if minute < 10 {
minuteString = "0\(minute)"
}
let second = (floorCounter % 3600) % 60
var secondString = "\(second)"
if second < 10 {
secondString = "0\(second)"
}
if time < 3600.0 {
timerLabel.text = "\(minuteString):\(secondString)"
} else {
timerLabel.text = "\(hour):\(minuteString):\(secondString)"
}
}
#objc func applicationDidBecomeActive() {
// handle event
lookForActiveTimers()
}
func lookForActiveTimers() {
var timers = [NSManagedObject]()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Timers")
//3
do {
timers = try managedContext.fetch(fetchRequest)
print("timers: \(timers)")
var activeTimer: NSManagedObject?
for timer in timers {
if let active = timer.value(forKey: "active") as? Bool {
if active {
activeTimer = timer
}
}
}
if let activeTimer = activeTimer {
// Handle active timer (may need to go to a new view)
if let closeDate = activeTimer.value(forKey: "appCloseTime") as? Date {
if let alreadyTimed = activeTimer.value(forKey: "alreadyTimed") as? Double {
let now = Date()
let difference = now.timeIntervalSince(closeDate)
// Handle set up again here
print("App opened with a difference of \(difference) and already ran for a total of \(alreadyTimed) seconds before close")
time = alreadyTimed + difference
startTimer()
}
}
} else {
print("We dont have any active timers")
}
// Remove active timers because we reset them up
for timer in timers {
managedContext.delete(timer)
}
do {
print("deleted")
try managedContext.save() // <- remember to put this :)
} catch {
// Do something... fatalerror
}
} catch let error as NSError {
print("Could not fetch. \(error), \(error.userInfo)")
}
}
#objc func applicationWillResign() {
// handle event
saveActiveTimer()
}
func saveActiveTimer() {
if isTimeRunning {
// Create a new alarm object
guard let appDelegate =
UIApplication.shared.delegate as? AppDelegate else {
return
}
let context = appDelegate.persistentContainer.viewContext
if let entity = NSEntityDescription.entity(forEntityName: "Timers", in: context) {
let newTimer = NSManagedObject(entity: entity, insertInto: context)
newTimer.setValue(true, forKey: "active")
let now = Date()
newTimer.setValue(now, forKey: "appCloseTime")
newTimer.setValue(self.time, forKey: "alreadyTimed")
do {
try context.save()
print("object saved success")
} catch {
print("Failed saving")
}
}
}
}
}
EDIT - Here is the full tested and working code on xCode 11.3 and a physical device iOS 13.2 - You have to figure out how to start and stop the timer according to your buttons - but this example simply starts the timer when the app is first opened and never stops or resets it.
You can reproduce this by creating a new single-view xCode project and replacing the code in the first view controller that it creates for you with the code above. Then create a label to attach to the outlet timerLabel on the VC
Also make sure to enable CoreData in your project while creating your new project * Then set up the entities and attributes in the xcdatamodel file:
Hope this helps
Related
I want to run timer in tableViewHeader part, the tableView header is containing textField ,label and button. To manage the timer I have created singleton class and it's working fine in the initial but when I tap on textField the timer getting stopped. I don't why. Should I manage it using separate thread?
//singleton class
class WorkoutTimerManager {
static let shared = WorkoutTimerManager()
private var sec = 0
private var min = 0
private var timer = Timer()
private var date: Date? = Date()
var timerLbl:UILabel?
private init() {
}
func startTimer() {
timer.invalidate()
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
}
func stopTimer() {
timer.invalidate()
}
#objc func updateTimer() {
if let date = self.date {
let elapsedSec = abs(Int(date.timeIntervalSinceNow))
self.sec = elapsedSec % 60
self.min = elapsedSec / 60
print("sdfs1 \(sec)")
if let timerLbl = timerLbl {
timerLbl.text = "\(sec)"
print("sdfs2 \(sec)")
}
}
}
}
// table view headerview delegate method
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let name = "EmptyWorkoutHeader"
guard
let nib = Bundle.main.loadNibNamed(name, owner: nil, options: nil)
else { fatalError("missing expected nib named: \(name)") }
guard
let headerView = nib.first as? EmptyWorkoutHeader
else { fatalError("view of type \(name) not found in \(nib)") }
WorkoutTimerManager.shared.timerLbl = headerView.timerLbl
WorkoutTimerManager.shared.startTimer()
return headerView
}
I am building an elapsed timer and while the code gives no errors the timer does not start.
I am using two ViewControllers, one called Stopwatch which has the start stop function in it under the class Stopwatch() and then a regular ViewController with the rest in it.
View Controller Code:
import UIKit
class ViewController: UIViewController {
let watch = Stopwatch()
#IBOutlet weak var elapsedTimeLabel: UILabel!
#IBAction func startButton(_ sender: Any) {
Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.updateElapsedTimeLabel), userInfo: nil, repeats: true)
watch.start()
}
#IBAction func stopButton(_ sender: Any) {
watch.stop()
}
#objc func updateElapsedTimeLabel (timer : Timer) {
if watch.isRunning {
let minutes = Int (watch.elapsedTime/60)
let seconds = watch.elapsedTime.truncatingRemainder(dividingBy: 60)
let tenOfSeconds = (watch.elapsedTime * 10).truncatingRemainder(dividingBy: 10)
elapsedTimeLabel.text = String (format: "%02d:%02d:%02d", minutes, seconds, tenOfSeconds)
} else {
timer.invalidate()
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override var prefersStatusBarHidden: Bool {
return true
}
}
The Stopwatch View Controller code:
import Foundation
class Stopwatch {
private var startTime : Date?
var elapsedTime: TimeInterval {
if let startTime = self.startTime {
return -startTime.timeIntervalSinceNow
} else {
return 0
}
}
var isRunning: Bool {
return startTime != nil
}
func start() {
startTime = Date()
}
func stop() {
startTime = nil
}
}
There is nothing at all coming in the debug window, so not sure what the issue is, I reconnected the buttons over and over so it's not that. I also get no other errors in the code as mentioned above.
Can anyone shed some light on this. Maybe I am using the wrong #selector or I am doing the updateElapsedTimeLabel minutes, seconds, tenOfSeconds calculations wrong. Not sure. Thanks for having a look.
If you Option-click on seconds and tenOfSeconds you will find that one is of type TimeInterval (i.e. Double) and the other is of type Double. So your format specifier of %02d was wrong. In C, a mismatch between the format specifier and the argument is undefined behavior. Swift doesn't say how it handles that but I guess it will ignore the argument.
To fix it, change your format specifier for the last 2 components to %02.f:
let minutes = Int(watch.elapsedTime/60)
let seconds = watch.elapsedTime.truncatingRemainder(dividingBy: 60)
let tenOfSeconds = (watch.elapsedTime * 100).truncatingRemainder(dividingBy: 100) // fixed the math here
elapsedTimeLabel.text = String(format: "%02d:%02.f:%02.f", minutes, seconds, tenOfSeconds)
But why not use a DateFormatter to make your life simpler:
class ViewController: UIViewController {
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "mm:ss:SS"
return formatter
}()
#objc func updateElapsedTimeLabel (timer : Timer) {
if watch.isRunning {
elapsedTimeLabel.text = formatter.string(from: Date(timeIntervalSince1970: watch.elapsedTime))
} else {
timer.invalidate()
}
}
}
I have a small elapsed timer in my game and it works very well. However I am trying to figure out how to save the elapsed time when you die so I can carry it over to the Game Over Screen where the Score and High Score is displayed.
I tired a few things but none of them seem to work. I guess it's because the time is not being saved anywhere when the it's game over, but rather just reset to 00:00:00 when the game restarts.
I use two view Controllers for this timer. One is called Stopwatch the other code is in the GameScene. Here are the codes.
I wanna bring it into a label like for example:
let timeLabel = SKLabelNode(fontNamed: "Planer")
timeLabel.text = "Time: \(savedTimer)"
timeLabel.fontSize = 100
timeLabel.fontColor = SKColor.white
timeLabel.zPosition = 2
timeLabel.position = CGPoint (x: self.size.width/2, y: self.size.height * 0.5)
self.addChild(timeLabel)*/
Stopwatch.swift code
import Foundation
class Stopwatch {
private var startTime : Date?
var elapsedTime: TimeInterval {
if let startTime = self.startTime {
return -startTime.timeIntervalSinceNow
} else {
return 0
}
}
var isRunning: Bool {
return startTime != nil
}
func start() {
startTime = Date()
}
func stop() {
startTime = nil
}
}
And the code I got speed out through my Game Scene:
import UIKit
class ViewController: UIViewController {
private let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "mm:ss:SS"
return formatter
}()
let watch = Stopwatch()
#IBOutlet weak var elapsedTimeLabel: UILabel!
#IBAction func startButton(_ sender: Any) {
Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.updateElapsedTimeLabel), userInfo: nil, repeats: true)
watch.start()
}
#IBAction func stopButton(_ sender: Any) {
watch.stop()
}
#objc func updateElapsedTimeLabel (timer : Timer) {
if watch.isRunning {
elapsedTimeLabel.text = formatter.string(from: Date(timeIntervalSince1970: watch.elapsedTime))
} else {
timer.invalidate()
}
}
override func viewDidLoad() {
super.viewDidLoad()
}
override var prefersStatusBarHidden: Bool {
return true
}
}
What I understand is that you're trying to save the elapsedTime of your watch after the user taps the stop button. If that's the case, in your stopButton function you are calling watch.stop(), which in turn resets the startTime = nil. So you might want to edit it like so:
// Create a new class variable to store the time
var savedTime: TimeInterval? = nil
#IBAction func stopButton(_ sender: Any) {
savedTime = watch.elapsedTime
// Use the savedTime here to pass to the game over function
watch.stop()
}
If you don't need to save the time in your ViewController class, you can move the savedTime variable to a local one in the stopButton function.
I'm building an app in Swift 3. When I press start the first time my timer begins, but when I pause it and try to press start again, the timer does not budge. To give context, the timer, with an amount of time attached to it, is selected from a table. each time the timer load, the start button works initially.
protocol TimerViewControllerDelegate: class {
func viewController(_ controller: ViewController, didFinishEditing item: TaskData)
}
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var timerLabel: UILabel!
#IBOutlet weak var pauseButton: UIButton!
#IBOutlet weak var startButton: UIButton!
#IBOutlet weak var timerTaskName: UILabel!
#IBOutlet weak var timerTimeSetting: UILabel!
#IBOutlet weak var progressView: UIProgressView!
weak var delegate: TimerViewControllerDelegate?
var timerTask: TaskData?
var timer: Timer?
var progressViewSpeed: Double = 0.0
#IBAction func cancel(_ sender: Any) {
timer?.invalidate()
dismiss(animated: true, completion: nil)
delegate?.viewController(self, didFinishEditing: timerTask!)
}
#IBAction func startButtonTapped(_ sender: Any) {
timerTask?.startTime = Date()
runTimer()
if timerTask?.isTaskRunning == true {
runTimer()
self.startButton.isEnabled = false
self.pauseButton.isEnabled = true
} else {
//retrieve start time and run
timerTask?.startTime = Date()
runTimer()
self.startButton.isEnabled = false
self.pauseButton.isEnabled = true
}
}
func runTimer() {
guard timer == nil else {
return
}
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
}
#IBAction func pauseButtonTapped(_ sender: UIButton) {
if timerTask?.isTaskRunning == true {
timer?.invalidate()
if let timerTask = timerTask, timerTask.isTaskRunning {
// Calculate the difference between now and when the timerTask was started
let difference = Int(Date().timeIntervalSince(timerTask.startTime!))
timerTask.taskRemaining -= difference
if timerTask.taskRemaining == 0 {
// Do something when there's no time remaining on the task?
}
timerTask.startTime = nil
}
}
else {
timerTask?.startTime = Date()
runTimer()
self.pauseButton.setTitle("Pause",for: .normal)
}
self.startButton.isEnabled = true
self.pauseButton.isEnabled = false
}
/*
#IBAction func resetButtonTapped(_ sender: Any) {
timer.invalidate()
seconds = 60
self.timerLabel.text = timeString(time: TimeInterval(seconds))
if self.resumeTapped == true {
self.resumeTapped = false
self.pauseButton.setTitle("Pause",for: .normal)
}
isTimerRunning = false
pauseButton.isEnabled = false
startButton.isEnabled = true
}
*/
func updateTimer() {
guard let timerTask = timerTask else {
return
}
if timerTask.taskRemaining < 1 {
timer?.invalidate()
timer = nil
//Send alert to indicate "time's up!"
} else {
updateTime()
}
progressViewSpeed = 1 / Double(timerTask.taskRemaining)
progressView.progress += Float(progressViewSpeed)
}
func timeString(time:TimeInterval) -> String {
let hours = Int(time) / 3600
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
return String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
override func viewDidLoad() {
super.viewDidLoad()
guard let timerTask = timerTask else {
return
}
if timerTask.isTaskRunning {
startButton.isEnabled = false
pauseButton.isEnabled = true
runTimer()
} else {
startButton.isEnabled = true
pauseButton.isEnabled = false
}
timerTaskName.text = timerTask.task
updateTime()
self.progressView.transform = CGAffineTransform.identity.rotated(by: CGFloat.pi / 2).scaledBy(x: 1, y: 150)
}
func updateTime() {
guard let timerTask = timerTask else {
return
}
if let startTime = timerTask.startTime {
// Calculate the difference between now and when the timerTask was started
let difference = Int(Date().timeIntervalSince(startTime))
if timerTask.taskRemaining == difference {
// Do something when there's no time remaining on the task
timer?.invalidate()
timer = nil
}
timerLabel.text = timeString(time: TimeInterval(timerTask.taskRemaining - difference))
} else {
timerLabel.text = timeString(time: TimeInterval(timerTask.taskRemaining))
}
}
}
Once you've invalidated an NSTimer, you can't use it again. You should create the new object.
See here for more From NSTimer Docs
Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed. Invalidating the timer immediately disables it so that it no longer affects the run loop. The run loop then removes and releases the timer, either just before the invalidate method returns or at some later point. Once invalidated, timer objects cannot be reused.
You need to invalidate it and recreate it. "isPaused" bool to keep track of the state
var isPaused = true
var timer: Timer?
#IBAction func pauseResume(sender: AnyObject) {
if isPaused{
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
pauseButton.isHidden = false
startButton.isHidden = true
isPaused = false
} else {
pauseButton.isHidden = true
startButton.isHidden = false
timer.invalidate()
isPaused = true
}
}
I have a Timer that i run on my app, it uses a save and reload feature for when the user leaves the view and returns to the view. However if i go to a different tab on my app, the actions needed for that tab seem to break the timer.
Would this be due to thread usage in the app and the coredata of the other tab breaking the timers counting? Here is my timer code
Update 2: Here is my revised timer code in full...
// MARK: - SETTING UP SETS & TIMERS
func createStopTimeForRestFromUserTime(userTime: Int) -> Date {
let calendar = Calendar.current
let stopDate = calendar.date(byAdding: .second, value: userTime, to: Date())
return stopDate!
}
func createTimer(stopDate: Date) {
print("CONSTRUCTING A TIMER")
userDefaults.set(stopDate, forKey: "setStopDate")
restTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(RestController.updateRestTimer), userInfo: nil, repeats: true)
}
func updateRestTimer() {
let presentTime = Date()
let stoppedTime = UserDefaults.standard.object(forKey: "setStopDate") as? Date
if stoppedTime?.compare(presentTime) == ComparisonResult.orderedDescending {
restRemainingCountdownLabel.text = dateComponentsFormatter.string(from: presentTime, to: stoppedTime!)
} else {
self.stopTimer()
print("THE TIMER IS NOW FINISHED")
}
}
func stopTimer() {
self.restTimer.invalidate()
}
// MARK: - CONFIGURE TIMER ON OPEN / CLOSE
override func viewWillDisappear(_ animated: Bool) {
print("VIEWWILLDISAPPEAR WAS CALLED")
stopTimer()
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ViewDidChangeNotification"), object: NSDate())
}
func handleResumedTime(disappTime: NSDate) {
let disappearedTime = disappTime
let resumeTime = NSDate()
if disappearedTime.compare(resumeTime as Date) == ComparisonResult.orderedDescending {
print("RESUMING THE TIMER")
self.createTimer(stopDate: disappearedTime as Date)
} else {
print("TIMER HAS FINISHED")
stopTimer()
}
}
func handleTimerCallback(notification: NSNotification) {
if let date = notification.object as? NSDate {
self.handleResumedTime(disappTime: date)
}
}
Code in an alternate VC id switch between which is causing breakage:
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(RoutineController.handleTimerCallback(notification:)), name: NSNotification.Name(rawValue: "ViewDidChangeNotification"), object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "ViewDidChangeNotification"), object: NSDate())
}
func handleTimerCallback(notification: NSNotification) {
let date = notification.object
print("Object is \(date)")
}
EDIT: Added the updated handleResumed func and console print out when changing tab and changing back again...
func handleResumedTime(disappTime: NSDate) {
let disappearedTime = disappTime as Date
let resumeTime = NSDate() as Date
print("ATTEMPTING RESUME")
print(disappearedTime)
print(resumeTime)
if resumeTime.compare(disappearedTime) == ComparisonResult.orderedAscending {
print("RESUMING THE TIMER")
self.createTimer(stopDate: disappearedTime)
} else {
print("TIMER HAS FINISHED")
stopTimer()
}
}
Try comparing the timers in milliseconds using DateFormatter and passing data between controllers using Notifications.
This function is called every time you switch tabs. It invalidates the timer, stops the ticking, and saves the time in milliseconds.
override func viewWillDisappear(_ animated: Bool) {
restTimer.invalidate()
let df = DateFormatter()
df.dateFormat = "y-MM-dd H:m:ss.SSSS"
let disappearingDate = Date()
let disappearingDateInMilliseconds = df.string(from: disappearingDate)
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "kViewDidChangeExampleNotification"), object: disappearingDateInMilliseconds)
}
/* add your Notification observers in each view controller */
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.handleTimerCallback(notification:)), name: NSNotification.Name(rawValue: "kViewDidChangeExampleNotification"), object: nil)
}
You need to define the function that will handle the notifications in each controller:
func handleTimerCallback(notification: NSNotification) {
if let dateString = notification.object as? String {
print("Object is an String: \(notification.object)")
resumeTimerWithDate(dateString)
} else {
print("Object is not of type String: \(notification.object)")
}
}
Quick example comparing Dates in Swift by milliseconds:
func resumeTimerWithDate(dateString: String) {
let df = DateFormatter()
df.dateFormat = "y-MM-dd H:m:ss.SSSS"
let secondDate = Date()
let secondDateToStr = df.string(from: secondDate)
if dateString < secondDateToStr {
print("RESUMING THE TIMER")
let resumeDate = df.date(from: dateString)
self.createTimer(resumeDate)
} else {
print("TIMER HAS FINISHED")
}
}
Other things to consider: You may want to add some print statements for debugging / sanity check because some functions may be firing before others. For example, rather than posting the notification in viewWillDisappear, you can try posting it in viewDidDisappear, or using prepare(for segue:).