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...
userDefault
in applicationDidEnterBackground
method.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!!
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()
}
}