Search code examples
iosswiftxcodetimersleep

Running a timer when the phone is sleeping


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

}

Solution

  • 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:

    CoreData setup

    Hope this helps