Search code examples
iosswiftnstimer

how to run NStimer in background Mode?


i am developing app that counts the work Hours for the employee.
so i want to run a timer which stay alive in backGround mode, but i did not found any way how to do this directly.

i think i can achieve this by following strategy...

  1. save current time in userDefault in applicationDidEnterBackground method.
  2. when application again become active, get saved time from userDefault, then find the difference with current Time in applicationWillEnterForeground method.Then display that calculated time.

now, i want to know, is this right way to do this? or is there any best way??
Thanks!!


Solution

  • There's no reason to save the current time on applicationDidEnterBackground.

    All you really need at any moment is the time the user “started work”, or nil if the user hasn't started work. Whenever you need to know the time worked, you can compute it as the current time minus the started-work time:

    let workStartTimeKey = "workStartTime"
    
    class ViewController: UIViewController {
    
        var secondsWorked: CFTimeInterval? {
            guard UserDefaults.standard.object(forKey: workStartTimeKey) != nil else { return nil }
            let startTime = UserDefaults.standard.double(forKey: workStartTimeKey)
            return CFAbsoluteTimeGetCurrent() - startTime
        }
    

    (Since the iOS SDK mostly uses the second as the unit of time, it's better to store and compute using seconds instead of hours. You can convert to hours if necessary for display or when communicating with your server or whatever.)

    I assume you have a button in your user interface for the user to tap when she starts work. When the button is tapped, you record the current time, start a timer to update the display periodically, and also update the display immediately:

        @IBOutlet var startWorkButton: UIButton!
    
        @IBAction func startWork() {
            guard secondsWorked == nil else {
                // Already started work.
                return
            }
    
            UserDefaults.standard.set(CFAbsoluteTimeGetCurrent(), forKey: workStartTimeKey)
            startDisplayUpdateTimer()
            updateViews()
        }
    
        private var displayUpdateTimer: Timer?
    
        private func startDisplayUpdateTimer() {
            displayUpdateTimer = Timer(timeInterval: 1, repeats: true, block: { [weak self] _ in
                self?.updateViews()
            })
            RunLoop.main.add(displayUpdateTimer!, forMode: .commonModes)
        }
    

    I assume you have another button to stop work. When it's tapped, you grab the current value of secondsWorked, clear the stored start-work time, cancel the display update timer, and do whatever you want with the grabbed value of secondsWorked. Here I'll just print it:

        @IBOutlet var stopWorkButton: UIButton!
    
        @IBAction func stopWork() {
            guard let secondsWorked = self.secondsWorked else {
                // Didn't start work yet!
                return
            }
    
            displayUpdateTimer?.invalidate()
            displayUpdateTimer = nil
            UserDefaults.standard.removeObject(forKey: workStartTimeKey)
            updateViews()
            print("seconds worked: \(secondsWorked)")
        }
    

    To update the display, you probably want to show the current time worked in a label. You can also enable or disable the buttons based on whether the user is currently working:

        @IBOutlet var timeWorkedLabel: UILabel!
    
        private func updateViews() {
            if let secondsWorked = secondsWorked {
                startWorkButton.isEnabled = false
                stopWorkButton.isEnabled = true
                timeWorkedLabel.text = timeWorkedFormatter.string(from: secondsWorked)
            } else {
                startWorkButton.isEnabled = true
                stopWorkButton.isEnabled = false
                timeWorkedLabel.text = "Not working"
            }
        }
    
        private let timeWorkedFormatter: DateComponentsFormatter = {
            let formatter = DateComponentsFormatter()
            formatter.allowedUnits = [.hour, .minute, .second]
            formatter.allowsFractionalUnits = true
            formatter.collapsesLargestUnit = true
            formatter.maximumUnitCount = 2
            formatter.unitsStyle = .abbreviated
            formatter.zeroFormattingBehavior = .dropLeading
            return formatter
        }()
    

    When the view is loaded, you should see if the user is already working. This can happen if the app was killed and relaunched after she started working. If she's already working, start the display update timer. Also you need to update the views regardless:

        override func viewDidLoad() {
            super.viewDidLoad()
            if secondsWorked != nil { startDisplayUpdateTimer() }
            updateViews()
        }
    

    Finally, you should cancel the timer when the view controller is destroyed, in case you have it in a navigation controller that allows it to be popped:

        deinit {
            displayUpdateTimer?.invalidate()
        }
    
    }