You said:
It seems as if 3 timers are started simultaneously and then the timer.invalidate stops all of them.
No. All three timers are running and when you call invalidate
you're just invalidating the last one. Each time you set timer
, you are discarding your reference to the prior one. Because you discarded your references to your first two timers, you now have no way to stop them. (Add a print
statement in fireTimer
and you will see the other two continue to fire after you cancel the one timer.) And because you invalidated on timer when timeOn
was zero, the other two will keep firing, with timeOn
now having negative values, and the == 0
test will never succeed again.
Instead, you could let your timer handler routine to accept a parameter, the timer reference. That way each one would be able to invalidate itself.
E.g.
func start() {
for interval in 0..< 3 { // if you really want three timers, then for loop is easiest
print ("Interval \(interval)")
Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(fireTimer(_:)), userInfo: nil, repeats: true)
}
}
@objc func fireTimer(_ timer: Timer) {
print(Date(), "tick")
timeOn -= 1
if timeOn <= 0 {
print("timer done")
timer.invalidate()
}
}
Now that is exceedingly confusing have multiple repeating timers all updating the same timeOn
property. I changed the if
test to be <=
0 to address that problem.
It begs the question why you would want multiple repeating timers firing at basically the same time. E.g. every second, timeOn
is being reduced by three. Is that really the intent? Generally you would only want one repeating timer.
This process of scheduling a bunch of timers also begs the question of how you will cancel them when the object in question is deallocated. I guess you could keep an array of them, but it seems very convoluted way of doing it. The other approach is to use the block-based timer with [weak self]
reference (to prevent strong reference cycle), and then each can check to see if self
is nil
and if so, invalidate itself:
func start() {
for interval in 0..< 3 { // if you really want three timers, then for loop is easiest
print ("Interval \(interval)")
Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else {
timer.invalidate()
return
}
self.fireTimer(timer, interval: interval)
}
}
}
func fireTimer(_ timer: Timer, interval: Int) {
print(Date(), "tick", interval)
timeOn -= 1
if timeOn <= 0 {
print("timer done")
timer.invalidate()
}
}
But I am unclear why you would want multiple repeating timers at all.
You said:
I don’t really want 3 timers. I want the timer block to run three times, sequentially.
Then just create a repeating timer that will run three times and then invalidate
itself:
weak var timer: Timer? // weak because when you schedule the timer, the RunLoop keeps a strong reference
deinit {
timer?.invalidate() // in case the timer is still running after you dismiss this object/controller
}
func start() {
timer?.invalidate() // in case you accidentally called this previously, cancel any prior timer (before you lose a reference to it)
var counter = 0
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
counter += 1
if counter >= 3 { timer.invalidate() }
self?.doSomething(counter) // do whatever you want here
}
}
Key things to note:
- I used single repeating timer.
- I used closure based rendition with
[weak self]
to avoid the strong reference cycle of the selector-based rendition of Timer
.
- If you’d like to keep a reference to the timer so that you can
invalidate
it as soon as the parent object is deallocated, keep your own weak
reference to it.
- In
start
, I invalidate
any prior timer, which is obviously unnecessary if you make sure that you call start
once and only once. But, again, it is a solid defensive programming pattern to invalidate
any prior timer before you replace any old reference with a new Timer
reference.