6

I have the following code:

class firstVC: UIViewController {
    var timer : Timer?

    func scheduledTimerWithTimeInterval(){
        timer = Timer.scheduledTimer(timeInterval: 60, target: self, 
        selector: #selector(self.anotherFunc), userInfo: nil, repeats: 
        true)
    }

    override func viewDidAppear(_ animated: Bool) {
        scheduledTimerWithTimeInterval()
    }
}

I'm trying to stop the timer without success:

func stopTimer() {
    if timer != nil {
        timer?.invalidate()
        timer = nil
    }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    stopTimer()
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    stopTimer()
}

I even tried to put the stop function in applicationWillResignActive And in applicationDidEnterBackground but it didn't stop:

firstVC().stopTimer()

Your help will be appreciated, thank you.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Shlomi
  • 337
  • 1
  • 6
  • 19
  • 2
    Did you try debugging and seeing if `timer?.invalidate` ever gets hit? – CodeNinja Oct 06 '17 at 18:01
  • 1
    Who calls scheduledTimerWithTimeInterval() ? If it gets called twice, you'll create 2 timers and lose track of the first one. Perhaps you should check for `nil` before creating the timer. – vacawama Oct 06 '17 at 18:06
  • No, but I do know that the timer is still running, I put a print log and it's showing every minute – Shlomi Oct 06 '17 at 18:06
  • viewDidAppear gets mostly called twice.. Check using breakpoints. – arvidurs Oct 06 '17 at 18:09
  • "No, but I do know that the timer is still running" ... Well, you really should test this. It eliminates one category of possible problems. You really do have to check to see if you're reaching your `invalidate` line. – Rob Oct 06 '17 at 20:12
  • If you employ DuncanC's solution, it solves one source of problem (duplicate `Timer` instances). And, you've said a number of times that despite two good answers, that your timer is still going. Well, perhaps you can clarify how precisely you're doing this (hitting the home button? dismissing this view? etc.). Also, do you have some background modes turned on? Bottom line, the answers below solve the obvious problem, but we need more detail if the problem is persisting. We need a reproducible example of the problem. – Rob Oct 06 '17 at 20:12
  • I put two things in 'applicationWillResignActive' func, 1. print log so I can see that the system calls it, 2. the stoptimer function, I can see the log that the 'applicationWillResignActive' was called but the timer is still running – Shlomi Oct 06 '17 at 20:20
  • Bottom line, timers that have `invalidate` called stop firing, plain and simple. So if you're seeing a timer continue to fire, it simply is not the one for which you called `invalidate`. I'd suggest you create a [stand-alone example that reproduces your problem](http://stackoverflow.com/help/mcve) and upload it somewhere for us to take a look at. Or check the address of the timer you invalidate and compare it to the timer that is firing, and you'll undoubtedly see that they're not the same timer instance. – Rob Oct 07 '17 at 18:25

5 Answers5

12

As others have said, you are creating multiple timers without killing the old one before starting a new one. You need to make sure you stop any current timer before starting a new one.

What I do is to make my timers weak. I then use the code `myTimer?.invalidate() before trying to create a new timer:

class firstVC: UIViewController {
    weak var timer : Timer?

    func scheduledTimerWithTimeInterval(){
        timer?.invalidate()
        timer = Timer.scheduledTimer(timeInterval: 60, target: self, 
        selector: #selector(self.anotherFunc), userInfo: nil, repeats: 
        true)
    }
}

By making your timer weak, the only thing that keeps a strong reference to the timer is the run loop. Then, when you invalidate it, it immediately gets released and set to nil. By using optional chaining to call the invalidate method, it doesn't do anything if it's already nil, and stops it and causes it to go nil if it IS running.

Note that this approach only works if you create your timer in one shot using one of the scheduledTimer() factory methods. If you try to create a timer first and then add it to the run loop, you have to use a strong local variable to create it or it gets released as soon as you create it.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
2

The problem is your timer gets instantiated more than once, so the original timer loses its reference. Add a variable didStartTimer = false. And then in viewDidAppear do a validation, and then call the timer func. That should do it.

like this:

class firstVC: UIViewController {
var timer : Timer?
var didStartTimer = false

func scheduledTimerWithTimeInterval(){
    timer = Timer.scheduledTimer(timeInterval: 60, target: self, 
    selector: #selector(self.anotherFunc), userInfo: nil, repeats: 
    true)
}

override func viewDidAppear(_ animated: Bool) {
            if !didStartTimer {
                 scheduledTimerWithTimeInterval()
                 didStartTime = true
            }

}
arvidurs
  • 2,853
  • 4
  • 25
  • 36
  • 1
    There's no need for the `didStartTimer` variable. Simply check the `timer` variable. – rmaddy Oct 06 '17 at 18:30
  • I am not so sure, because viewDidAppear will mostly called twice, hence I am validating it to just have one timer instance running. – arvidurs Oct 06 '17 at 18:35
  • 2
    And you can simply check if `timer` is nil or not. No need for the extra variable. – rmaddy Oct 06 '17 at 18:36
2

After research I found a solution,
The only thing that worked for me is to create functions in AppDelegate file and call them when needed,
Here is the code, the timerSwitch function:

    func timerSwitch()
    {
        if (timerStatus) {
            checkStateTimer = Timer.scheduledTimer(
                timeInterval: 60, 
                target:self, 
                selector: #selector(self.yourFunction), 
                userInfo: nil, repeats: true)
        } else {
            checkStateTimer?.invalidate()
        }
    }

    func stopTimer()
    {
        timerStatus = false
        timerSwitch()

    }

    func startTimer()
    {
        timerStatus = true
        timerSwitch()

    }

While 'yourFunction' is what you want to execute when the timer starts,
In my case is sending heartbeat.
Then I called the timerSwitch is the following functions in AppDelegate:

    func applicationWillResignActive(_ application: UIApplication) {
        stopTimer()
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        stopTimer() 
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        startTimer()
    }
Shlomi
  • 337
  • 1
  • 6
  • 19
1

Nothing works for me, at last using self did the trick!!!

 self.atimer?.invalidate()
 self.atimer = Timer()
guru
  • 2,727
  • 3
  • 27
  • 39
  • I didn't down-vote you, but I suspect that your use of the force-unwrap might be the reason. If `atimer` is nil, that code will crash. Better to use the optional chaining form `self.atimer?.invalidate()`. That code will safely do nothing if the timer is nil, and invalidate it if it is not nil. – Duncan C Sep 27 '19 at 15:52
  • Thanks Duncan, You are right it was my mistake, i will edit the answer. – guru Sep 28 '19 at 06:14
  • I'd also recommend using the Timer convenience methods `scheduledTimer(timeInterval:target:selector:userInfo:repeats:)` or `scheduledTimer(withTimeInterval:repeats:block:)`. If you use the default initializer `Timer()` you then have to configure the timer and add it to the run loop. The `scheduledTimer` methods do everything in one step. – Duncan C Sep 28 '19 at 18:53
  • It works, omg, apple's awkward tricks. – Yılmaz edis Mar 01 '23 at 20:39
0

As a debugging step, try putting var timer = Timer() in the global space (e.g. at the top of the file below the import statements) to make sure there's only one Timer object being created and referred to. If you have the Timer declaration within a function, you'll make a new timer each time you call that function, which causes you to lose the reference to the old one, and ultimately not stop it.