comments 5

#9 How to invalidate NSTimer properly?

NSTimer is a useful little one. It waits for some time and then fires, can fire periodically and runs in the run loop. There is also one unexpected feature. NSTimer retains its target. It’s not a big deal when you use a non-repeating timers as they get invalidated automatically after firing. However, when you use a repeating timer and forget about retaining, this means trouble.

This post includes 2 ways applied by my team to deal with that issue. There is also a very simple trick to detect whether you have the retaining timer problem or not.

Our case

In our project we have a Facebook style wall with posts and status updates. As this is a very interactive part of an app it should be refreshed quite often. You can probably recall quite easily one of your cases.

NOTE: Problem is universal for any architecture of iOS app. You may find some articles for MVC and others, where UIViewControllers or other Controllers are responsible for NSTimer lifecycle. That is why we have decided to present examples with VIPER architecture. In this scenario an Interactor is responsible for creating and invalidating NSTimer.

How NOT to do it: Invalidate in deinit

Problem in our code was detected during a code review of one of pull requests in repository. If you maintain NSTimer this way, your target, in our case an Interactor will never get deinitialised:

All trouble starts at the moment of passing self as a target. NSTimer holds a strong reference to WallInteractor instance. Even that we exit the wall screen, so that all other references are removed this one remains.

As stated in Apple’s documentation invalidate() method is the only way to remove NSTimer from a run loop and remove strong references to target and userInfo objects.

Do I have the same problem?

A quick way to verify whether problem is existing in your setup is to use a simple print debugging method:

As we had assigned self as a target of NSTimer and had been trying to invalidate timer in deinit we have never seen that statement printed in a console.

Solution #1: Invalidate on view disappear events

One way to tackle this issue is to use viewWillDisappear() or viewDidDisappear() events to invalidate NSTimer. If you use UIViewController to handle NSTimer then it is pretty straightforward

In case of using VIPER architecture and handling NSTimer in Interactor we had to propagate that event from View through Presenter to Interactor:

As you can see, passing such a simple information through half of the VIPER structure may be one of the reasons to start looking for an alternative. You could use notifications but that would not be the cleanest solution.

Solution #2: Wrap self in an intermediate object

For some reasons you cannot or may not want to use view disappear events. Solution #1 solved the problem by moving invalidate() call to a different place. There is also possibility to deal with the problem by not giving self as NSTimer‘s target. How? By using a wrapper class. For example:

What happens in the code above?

Firstly, when scheduling NSTimer we pass an intermediate object TimerTargetWrapper as a target, that has a weak reference to our interactor.

Secondly, when entire VIPER module is destroyed and it comes to deiniting Interactor it has just a weak reference from TimerTargetWrapper left. In this way deinit gets called and NSTimer gets invalidated.

As mentioned above NSTimer after invalidation releases reference to its target, it this case the TimerTargetWrapper. Everything gets cleaned up.

Summary

Hopefully you will find this article a useful reference for problems with invalidating NSTimer. Clearly it does not cover all edge cases and possible solutions to the problem. Additionally, we wanted to share some more insights from our work with VIPER architecture.

You can find some useful references below:

5 Comments

  1. Jeff Gilbert

    I like that you show how the first instinct for a solution might not be the best solution.

    You might also consider not having your interactor dependent on a concrete class like NSTimer, but rather an abstract role like an Updater. By using a test updater (that you control) in your tests you can ensure the interactor responds properly when it is told to update its content. This means your test can execute in milliseconds, rather than 15 seconds.

    How/when an update is triggered is outside the responsibility of the interactor. In your production code, you could have a TimedUpdater that uses NSTimer to determine when to trigger an update.

    • Michał Wojtysiak

      Hi Jeff. Very good point. Abstracting timer functionality, using wrapper class, would bring many benefits for unit testing. Thank you!

      Best regards,
      swifting.io

  2. Blade

    Solution #2 should work well in theory, however, in my following code, it doesn’t seem to have any effect. Any ideas what I might be doing wrong?. The line “Counter deinited” is never printed. (Btw, this is a command-line project)

    import Foundation

    class IncrementingCounter {

    private class TimerTargetWrapper {
    weak var target: IncrementingCounter?
    init(target: IncrementingCounter?) {
    self.target = target
    }
    @objc func tick(timer: Timer) {
    target?.incrementCounter()
    }
    }

    private var counter = 1
    private var timer: Timer?

    deinit {
    print(“Counter deinited”)
    timer?.invalidate()
    }

    func start() {
    print(“Starting timer”)
    guard timer == nil else { return }
    timer = Timer.scheduledTimer(timeInterval: 1,
    target: TimerTargetWrapper(target: self),
    selector: #selector(TimerTargetWrapper.tick(timer:)),
    userInfo: nil,
    repeats: true)
    print(“Scheduled timer”)
    }

    @objc private func incrementCounter() {
    print(counter)
    counter += 1
    }
    }

    var counter: IncrementingCounter? = IncrementingCounter()
    counter?.start()

    DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) { [weak counter] in
    print(“Setting counter to nil”)
    counter = nil
    }

    CFRunLoopRun()

Leave a Reply


*