How to update a label every time minute changes? - ios

So I've been searching Stackoverflow for a good solution on how to update label if the time change, but so far the results have been unsatisfactory. Most use a timer, and that's not what I want.
What I want is if the status bar time is 8:52 PM and if some arbitrary bus leaves at 9:00 PM then I want the label to show 8 min. Then, if the time changes to 8:53 PM I want the label to show 7 min.
I'd prefer some sort of notification or delegation method rather than setting a timer.
If anyone has any suggestions on what I should do (or any 3rd party libraries that could notify if the time changes) that would be great!

You only have limited choices. If your app is not running in the foreground, you're limited to local notifications, which display an alert to the user. Using those once a minute would be awful for the user.
When your app comes to the foreground you can start a timer that is timed to fire every minute on the minute and use that to update a label in your app:
weak var timer: Timer?
func startTimer() {
timer?.invalidate()
let interval = Double(Date().timeIntervalSinceReferenceDate)
let delay = 60 - fmod(interval, 60.0)
message.text = "Delay = \(delay)"
//Create a "one-off" timer that fires on the next even minute
let _ = Timer.scheduledTimer(withTimeInterval: delay, repeats: false ) { timer in
self.message.text = "\(Date())"
self.timer = Timer.scheduledTimer(withTimeInterval: 60.0,
repeats: true ) { timer in
//Put your repeating code here.
self.message.text = "\(Date())"
}
}
}

Related

how to create countdowns in swift that will start after a countdown has finished

[New to Swift]
I am making an app on Swift 5 that displays time left before my next task is to start.
My app:
Task 1: Start at 9.30 am
Task 2: Start at 10.15 am
Let's say Current Time: 09.00 am
Countdown: Time before next task start = 00:30:02 --> 00:30:01 ...
I would like to display countdown of the nearest task.
Currently, I can only have one countdown timer on my viewonLoad() that picks up the current countdown. It continues and once it finishes it does not start next timer after it has finished. I understand I have to deal with Background state at a later date, but since I am slowly starting. My idea is to make sure I can initiate next countdown timer once my current one has expired.
So at any point when I open my app, it will always display countdown till my next task.
var count = 30 // calculation simplified ... this data is calc from two different date obj
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(update), userInfo: nil, repeats: true)
}
#objc func update() {
if(count > 0) {
count = count - 1
countdownLabel.text = String(count)
}else{
timer!.invalidate()
getNextTaskTimes()
}
}
func getNextTaskTimes(){
count = 5 // not working
}
How can I achieve this please? Also, is there a better way of doing this? Given that, my user may choose to close the app and come back at a later time, I still would want my countdown to continue in the background.
Any help is appreciated. :D Thanks
I realized the above code works. I just need to make sure I do not invalidate the timer in the else block. Invalidating the timer just kills it.

Why is my timer's tolerance always halved?

In my app, I'm scheduling a timer like this:
let period = TimeInterval(10)
let timer = Timer.scheduledTimer(withTimeInterval: period, repeats: true, block: { [weak self] (_) in
// Some repeating code here
})
timer.tolerance = period
Essentially, I want a timer to fire once in every consecutive, repeating 10 second period, but it doesn't matter when the timer fires in each individual period. However, if I set a breakpoint in the debugger for immediately after this code runs. I can see that my timer's timeInterval is set to 10 seconds, but the timer's tolerance is set to 5. I've played around with various values for period, but no matter the case, it seems that my timer's tolerance will always be half of its timeInterval. Why is that? Will this still produce the functionality I intend? If not, how can I prevent this from happening?

Swift stopwatch running slower than expected [duplicate]

This question already has answers here:
NSTimer Too Slow
(2 answers)
Closed 4 years ago.
I am trying to implement a stopwatch into my app, but I've noticed that it actually runs slower than it should. Here is the code:
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(display), userInfo: nil, repeats: true)
func stringFromTimeInterval(interval: TimeInterval) -> NSString {
let ti = Int(interval)
let minutes = ti / 6000
let seconds = ti / 100
let ms = ti % 100
return NSString(format: "%0.2d:%0.2d.%0.2d",minutes,seconds,ms)
}
#objc func display() {
interval += 1
lapInterval += 1
timeLabel.text = stringFromTimeInterval(interval: TimeInterval(interval)) as String
lapLabel.text = stringFromTimeInterval(interval: TimeInterval(lapInterval)) as String
}
Hopefully I've included enough information. Thanks in advance!
Don't try to run a timer that fires every hundredth of a second and then count the number of times it fires. Timers are not exact. Their resolution is more like 50-100 milliseconds (0.05 to 0.1 seconds), and since a timer fires on the main thread, it depends on your code servicing the event loop frequently. If you get into a time-consuming block of code and don't return, the timer doesn't fire.
Plus, the screen refresh on iOS is every 1/60th of a second. There's no point in running a timer more often than that, since you won't be able to display changes any faster.
Run a timer more like every 1/30 of a second, and calculate the elapsed time each time it fires as described below:
To calculate elapsed time, record the time (With Date() or Date().timeIntervalSinceReferenceDate) when you want to begin timing, and then every time you want to update, calculate 'new_date - old_date'. The result will be the number of seconds that have elapsed, and that will be exact and with Double precision.

Intermittent Missing Seconds with Timer

I have a timer, which is a singleton, that repeatedly fires every second. I allow the user to pause the timer as well as resume. I am keeping track of the start date of the timer, and I am subtracting any pauses from the elapsed time.
Unfortunately, I can't seem to fix an intermittent issue where pausing and resuming the timer causes a skipping of one second.
For instance, in the following code block, I start the timer and print the seconds:
1.0
2.0
3.0
4.0
5.0
6.0
7.0
8.0
9.0
10.0
11.0
12.0
13.0
14.0
15.0
16.0
17.0
18.0
19.0
In the following code block, I resume the timer, printing the seconds again. However, as you can see, the 20th second has not been printed:
21.0
22.0
23.0
24.0
25.0
26.0
I cannot seem to figure out where I am losing the second. It does not happen with each pause and resume cycle.
The properties that I am using to keep track of the aforementioned are as follows:
/// The start date of the timer.
private var startDate = Date()
/// The pause date of the timer.
private var pauseDate = Date()
/// The number of paused seconds.
private var paused = TimeInterval()
/// The number of seconds that have elapsed since the initial fire.
private var elapsed = TimeInterval()
I start the timer by creating the timer and setting the start date:
/// Starts the shower timer.
func startTimer() {
// Fire the timer every second.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
// Set the start time of the initial fire.
startDate = Date()
}
If the user pauses the timer, then the following method executes:
/// Pauses the shower timer.
func pauseTimer() {
// Pause the timer.
timer?.invalidate()
// Set the timer to `nil`, according to the documentation.
timer = nil
// Set the date of the pause.
pauseDate = Date()
}
Then, the following method executes when the user resumes the timer:
/// Resumes the timer.
func resumeTimer() {
// Recreate the timer.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
// Add the number of paused seconds to the `paused` property.
paused += Date().timeIntervalSince(pauseDate)
}
The following method, which is called by the method that executes when the timer fires, sets the number of elapsed seconds since the initial fire, less the sum of any pauses:
/// Sets the number of elapsed seconds since the timer has been started, accounting for pauses, if any.
private func updateElapsedTime() {
// Get the date for now.
let now = Date()
// Get the time that has elapsed since the initial fire of the timer, and subtract any pauses.
elapsed = now.timeIntervalSince(startDate).rounded(.down).subtracting(paused.rounded(.down))
}
Finally, the following method is the Selector that executes when the timer fires:
/// Updates the number of elapsed seconds since the timer has been firing.
#objc private func updateElapsedSeconds() {
// Configure the elapsed time with each fire.
updateElapsedTime()
// Post a notification when the timer fires, passing a dictionary that includes the number of elapsed seconds.
NotificationCenter.default.post(name: CustomNotification.showerTimerFiredNotification, object: nil, userInfo: nil)
}
What am I doing incorrectly to cause a missing second intermittently?
So the issue here is that Timer is not accurate in this way. Or rather, its timekeeping is reasonably accurate, but the actual rate of firing has some variance as it is dependent on the runloop.
From the documentation:
A timer is not a real-time mechanism; it fires only when one of the
run loop modes to which the timer has been added is running and able
to check if the timer’s firing time has passed.
To show this, I got rid of all of the rounding in your code and printed the output (you don't even need to pause to see this happen). Here is what this variance looked:
18.0004420280457
19.0005180239677
20.0004770159721
21.0005570054054
21.9997390508652
23.0003360509872
24.0003190040588
24.9993720054626
25.9991790056229
Sometimes it fires particularly late and this causes the whole thing to get thrown off. The rounding doesn't help because you are still depending on the timer for the actual reference time and eventually it will be off by more than a second.
There are a few ways to fix the situation here depending on what exactly you are trying to accomplish. If you absolutely need the actual time, you can adjust the timer to fire at fractions of a second and instead use that output to estimate the seconds a little more accurately. This is more work and will still not be totally right (there will always be a variance).
Based on your code, it seems like simply incrementing a number based on the timer should be enough to accomplish your goal. Here is a simple modification to your code making this work. This will count up simply and never skip a second in the count whether you pause or not:
/// The number of seconds that have elapsed since the initial fire.
private var elapsed = 0
private var timer: Timer?
/// Starts the shower timer.
func startTimer() {
elapsed = 0
// Fire the timer every second.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
}
/// Pauses the shower timer.
func pauseTimer() {
// Pause the timer.
timer?.invalidate()
// Set the timer to `nil`, according to the documentation.
timer = nil
}
/// Resumes the timer.
func resumeTimer() {
// Recreate the timer.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateElapsedSeconds), userInfo: nil, repeats: true)
}
/// Sets the number of elapsed seconds since the timer has been started, accounting for pauses, if any.
private func updateElapsedTime() {
// Get the time that has elapsed since the initial fire of the timer, and subtract any pauses.
elapsed += 1
// debug print
print(elapsed)
}
/// Updates the number of elapsed seconds since the timer has been firing.
#objc private func updateElapsedSeconds() {
// Configure the elapsed time with each fire.
updateElapsedTime()
// Post a notification when the timer fires, passing a dictionary that includes the number of elapsed seconds.
NotificationCenter.default.post(name: CustomNotification.showerTimerFiredNotification, object: nil, userInfo: nil)
}

Swift - Best way to run timer in background for iphone apps

I'm totally new to iOS development and am working on an iPhone cooking app that gives the user the choice of three 'timer' options. The first timer runs for 6 mins, the second for 8.5 mins and the last for 11 mins.
Once the timer finishes counting down it plays an audio file and displays a message within the app screen. Everything works perfectly, except that I've discovered in testing that the timer stops running while the user goes to another app (e.g. checking email, using Safari, etc). Obviously, this defeats the purpose of the app as the user needs to know when the timer is finished so they can do the next step (e.g. remove a saucepan from the stove).
I've researched background modes and am getting confused. It seems that I literally have no reason (according to Apple) to run this app in the background (i.e. it's not playing music, using locations services, etc). I also keep reading that there's a 10 min limit to running in the background otherwise.
I also come across the idea of local and remote notifications, but the page I was referred to no longer exists on Apple's developer site. I'm now at a loss and confused.
Is there a way for me to actually get this app to work in the background for up to 11 minutes? If so, how?
Here's an update. I've been trying to get my head around Local Notifications and Background Tasks.
LOCAL NOTIFICATIONS
This showed some promise, but I'm not sure how I would implement this in my scenario? How would I ensure the right amount of time passes before the notification appears/plays a sound?
For example, the user selects the button for 'soft boiled eggs' at exactly 12:00:00pm and the app starts a counter for 6 mins. At 12:01:20pm the user reads an email, taking 30 seconds before putting the phone down at 12:01:50 to read the paper. Let's assume at 12:02:50 the phone goes into lock mode, how do I ensure the local notification triggers 3mins and 10secs later to make up the whole 6mins and play the sound file notifying the user their eggs are ready.
BACKGROUND TASKS
This may work for my scenario if I can start and restart background tasks to allow my timer to complete before playing the sound.
Below is a snippet of my code (relating to the eggs example above) that I hope will help put my app in context:
#IBAction internal func ButtonSoft(sender: UIButton) {
counter = 360
TimerDisplay.text = String("06:00")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
#IBAction internal func ButtonMedium(sender: UIButton) {
counter = 510
TimerDisplay.text = String("08:30")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
#IBAction internal func ButtonHard(sender: UIButton) {
counter = 660
TimerDisplay.text = String("11:00")
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("updateCounter"), userInfo: "Eggs done!!", repeats: true)
ButtonSoft.alpha = 0.5
ButtonMedium.alpha = 0.5
ButtonHard.alpha = 0.5
ButtonSoft.enabled = false
ButtonMedium.enabled = false
ButtonHard.enabled = false
}
func stopTimer() {
if counter == 0 {
timer.invalidate()
}
}
func updateCounter() {
counter--
let seconds = counter % 60
let minutes = (counter / 60) % 60
let strMinutes = minutes > 9 ? String(minutes) : "0" + String(minutes)
let strSeconds = seconds > 9 ? String(seconds) : "0" + String(seconds)
if seconds > 0 {
TimerDisplay.text = "\(strMinutes):\(strSeconds)"
}
else {
stopTimer()
TimerDisplay.text = String("Eggs done!!")
SoundPlayer.play()
}
}
#IBAction func ButtonReset(sender: AnyObject) {
timer.invalidate()
stopTimer()
TimerDisplay.text = String("Choose your eggs:")
ButtonSoft.alpha = 1.0
ButtonMedium.alpha = 1.0
ButtonHard.alpha = 1.0
ButtonSoft.enabled = true
ButtonMedium.enabled = true
ButtonHard.enabled = true
}
In terms of running background tasks, I've come across the following example of code.
To create the background task:
func someBackgroundTask(timer:NSTimer) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), { () -> Void in
println("do some background task")
dispatch_async(dispatch_get_main_queue(), { () -> Void in
println("update some UI")
})
})
}
And the below line of code to (I think) use a timer to run the above function:
var timer = NSTimer(timeInterval: 1.0, target: self, selector: "someBackgroundTask:", userInfo: nil, repeats: true)
And the below code to stop it all:
timer.invalidate()
So, how would I adapt this for my scenario? If this isn't possible, how would I use local notifications in my app?
Or do I just give up on the iPhone version of this app (my Apple Watch version seems to work fine).
In a word, no. You can ask for background time, but recent versions of iOS give you 3 minutes.
If you are a background sound playing app or navigation app you are allowed to run in the background for longer, but you have to ask for those permissions and the app review board will check.
The bottom line is that third parties can't really do a timer app that counts down an arbitrary time longer than 3 minutes.
You might want to use timed local notifications. You can make those play a sound when they go off. Search in the Xcode docs on UILocalNotification.
Edit in Aug 2020: I would no longer recommend this approach.
I have had some success by starting a background task, then setting a timer for just under a minute. When the timer fires, I start a new background task and end the old one. I just keep rolling over the background task, creating a new one every minute.

Resources