doing it wrong

Blogging with Class

nstimer for fun and profit

Let’s talk about NSTimer. It’s a core class that every Apple developer needs to use. And yet, despite that, it’s still misused regularly. We’ll start with the basics.

Using a NSTimer is relatively easy. You just need to create an NSTimer object and make sure it’s added to the runloop you want it to fire on. Of course, it’s a little more difficult than that because Apple provides multiple ways to go about this task. In addition, Apple also provides two ways to get notified when the timer fires. Let’s take a closer look at the three ways we have of creating an NSTimer.

  • scheduledTimerWithTimeInterval:invocation:repeats:
  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:

These are the easiest of the three ways of creating a NSTimer. You simply call this method and your timer will be allocated, initialized and scheduled on the current run loop.

  • timerWithTimeInterval:invocation:repeats:
  • timerWithTimeInterval:target:selector:userInfo:repeats:

These methods will only handle the allocation and initialization of the NSTimer. You will need to manually add the timer to a runloop.

  • initWithFireDate:interval:target:selector:userInfo:repeats:

This method needs to be called on an NSTimer that you have already called alloc on. You will still need to manually add the timer to a runloop. There is no reason to use this method for new code, unless you just like to call alloc yourself.

You should’ve noticed that there are two versions of most of those methods. One version takes an NSInvocation object, the other takes a target, a selector and something called userInfo. If you take a look at the NSInvocation class, you can see that it’s a wrapper around a target and a selector, along with the ability to pass arguments as well. Conceptually, you can think of the second version with the target, selector and userInfo as a wrapper around the first that will wrap your target and selector into a NSInvocation with the userInfo as the only parameter. There are almost certainly differences in the actual implementation, but it should behave as if it was implementated that way. You can choose either of them, but I’ve found that the using the target, selector and userInfo version is typically the easier way to go.

You might be wondering why you wouldn’t just always have it schedule the timer on the current run loop? Well it’s possible that you are configuring a timer that needs to be attached to a run loop on a different thread. If you were running on a different thread than the main thread and your timer needed to update UI state, you would want to make sure that the timer ran on the main thread and not the current thread. If you do need to handle this, you will need to call addTimer:forMode: on the desired NSRunLoop to schedule it.

One thing you didn’t see as an option above is a blocks based method for timers. Apple hasn’t (as of Nov 2015) added blocks APIs to NSTimer. Which means that by default, NSTimers will be a little tougher to use than they probably should be. If you really want a blocks API for NSTimer, it’s fairly trivial to implement one using a category. There are also many implementations on the web, so go check those out if your’re interested.

So now you’ve created an NSTimer – now what? If you created a one-shot timer (i.e, repeats is NO) and there is no need for you to cancel the timer later, you are pretty much done once it’s been scheduled on a run loop. You can even ignore the NSTimer object that was returned to you (if you are using ARC, that is. You are using ARC, right?). If you have a repeating timer or you need the ability to cancel the timer, then you need to keep a reference to the NSTimer. But how?

This is one of the trickier bits about NSTimer. The normal Cocoa way would be to put it into a strong reference variable somewhere and then reference it as needed. You might even set it to nil in your dealloc. And this is almost certainly always wrong for NSTimer.

If you look at the documentation, you’ll find out that NSTimer holds a strong reference to the target (either directly or through the NSInvocation object). If that target happens to be the class that is holding a strong reference to the NSObject, bad things will happen. You’ve just created a circular reference between the two objects and they will never be released.

“But I need the timer to stick around. I can’t just put it into a weak reference variable and expect it to stay around!” Actually, you can. That’s because once it’s been scheduled on a runloop, the runloop now has a strong reference to it. Which means you can use a weak reference and you’ve now broken the circular reference cycle.

So if you have a weak reference to the NSTimer, how would you actually go about canceling it? This is where the invalidate methods comes in. You can call invalidate on a NSTimer object, and that will cause the runloop that it is scheduled on to release it. Since you have a weak reference, your reference will automatically be set to nil and nothing will leak.

Also if you are not automatically adding your NSTimer to your current run loop, you will need a temporary strong reference to hold your NSTimer object while you configure it. Hopefully this example will make this a bit clearer:

  // in your @interface
  @property(nonatomic, weak, readwrite) NSTimer *myTimer;

  // in your method where you create the timer
  NSTimer *tmpTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
  [[NSRunLoop mainRunLoop] addTimer:tmpTimer forMode:NSDefaultRunLoopMode];
  self.myTimer = tmpTimer;

If you didn’t have a strong reference to your created timer before it was added to a run loop, you would quickly find out that you didn’t have a timer object anymore. Without it being added to a runloop, nobody is holding a reference. So you need to make sure you have a strong reference at least until you’ve added it.

One more note about NSTimer. Under no circumstances should you ever attempt to invalidate your timer in the dealloc method of the target object. And the reason should be obvious now. The NSTimer has a strong reference to your target object, which means that dealloc will never be called as long as the NSTimer is scheduled. You should always invalidate in some other method that can be called before the object is expected to go away. For example, if you have a timer in a UIViewController subclass, you can use viewWillAppear and viewWillDisappear as the place to create and invalidate your timers.

Hopefully you’ve learned a little more about NSTimer now. It’s a simple class with some subtle corner cases. But with just a little bit of understanding of how it works, you can keep yourself out of those corners.

Nov 20, 2015

Copyright © 2015-2021 Dennis Munsie.