1

I am using a Swift Timer which works fine until I try putting it into a loop. I start the timer and after it reaches zero the selector calls a method to invalidate the timer. I want this to be repeated for 3 times and have a counter that counts the number of iterations this goes through.

func start() {
    var interval = 0
    repeat {
       interval += 1
       print ("Interval \(interval)")
       timer = Timer.scheduledTimer(timeInterval: 1.0, target: self,    selector: #selector(fireTimer), userInfo: nil, repeats: true)
        } while (interval <= 3)
    }
    @objc func fireTimer() {
        timeOn  -= 1
        if timeOn == 0 {
            print("timer done")
            timer?.invalidate()
        }
    }

The output is:

Interval 1 Interval 2 Interval 3 timer done

It seems as if 3 timers are started simultaneously and then the timer.invalidate stops all of them. What I want is to have the timers start independently and run sequentially. Notice the repeat/while loop. Any suggestions appreciated.

1 Answers1

4

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:

  1. I used single repeating timer.
  2. I used closure based rendition with [weak self] to avoid the strong reference cycle of the selector-based rendition of Timer.
  3. 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.
  4. 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.
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I thanks for the reply. I don’t really want 3 timers. I want the timer block to run three times, sequentially. – Tom Springett Jan 17 '21 at 04:19
  • Then it’s simplified a bit. I’ve edited my answer to include the “single repeating timer that fires three times” solution (including a few refinements that are prudent). – Rob Jan 17 '21 at 05:16
  • Thanks you for your detailed and thoughtful answer. It may help if I explain better what I am looking for. I want a count down timer that repeats. So the timer fires for 10 seconds (for example) then starts again and repeats for another 10 seconds for x number of repeats. The count down timer displays seconds remaining and the current number of repeats. Note that I am using the word repeats here in a different context than the timer argument. Your last code above works, but for only the first 'repeat'. – Tom Springett Jan 17 '21 at 22:16
  • OK, that helps understand why you were thinking of multiple timers. Still, though, if it’s going to immediately start counting down automatically, you would probably only have the one `Timer` instance, and just reset what you’re counting to. You could create a new instances, but unless you are waiting for user interaction before starting it again, that’s probably unnecessary complicated. – Rob Jan 17 '21 at 22:39
  • 1
    Thanks-You! My take-away from your comments lead me to focus on one timer and one instance. I now have it working! I learned a ton from your comments. Thanks again for your time. – Tom Springett Jan 18 '21 at 15:37