Event tracking stops NSTimer

NSTimers usually don’t fire when the user is scrolling a table or other UIScrollView – or doing anything else that puts the run loop in event tracking mode.  This post describes the problem and an easy solution.

The problem

Suppose you set up a timer in the most convenient manner, with something like this:

[NSTimer scheduledTimerWithTimeInterval:0.5
                                 target:self
                               selector:@selector(timerFired:)
                               userInfo:nil repeats:YES];

This is handy because the timer is added to the run loop for you, so there is no other setup required. However, if you check the docs, the timer is added in the default run loop mode (

NSDefaultRunLoopMode

). This sounds pretty safe, but it means your timers will effectively be frozen when the default run loop mode is on hold.

A little background on run loops modes: the system has a primary loop that checks for various inputs such as from a keyboard, the touch screen, and timers, and knows which pieces of the code to call (the observers) to notify of those inputs.  The setup is similar to an NSNotificationCenter, except that actions are often initiated outside your code, and the callbacks may not be called immediately.

If you think about it, there’s a need for prioritization in which inputs are responded to in certain situations.  For example, scrolling animations on the iPhone are very smooth despite limited processing power.  Part of the efficiency comes from ignoring lower priority inputs when scrolling (or other event tracking events) are happening.  Unfortunately, timers are one of the things that are ignored when they’re running in the default run loop mode.  The default mode is basically the lowest priority, but also the most often-run mode when nothing else is going on.  In a sense you can think of other modes as a sort of interrupt system, if you’re familiar with processor-level interrupts.

So the bottom line is that frozen timers during scrolling is actually the desired behavior by default, although in some cases you might want to keep your timers firing all the time.

The solution

To do this, simply add the NSTimer to your run loop directly and specify the common run loop modes (which will include event tracking mode, unless you do something to change that).  Code like this works:

  NSTimer *timer = [NSTimer timerWithTimeInterval:0.5
                                           target:self
                                         selector:@selector(timerFired:)
                                         userInfo:nil repeats:YES];
  [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

Resources

I first saw this solution on stackoverflow (thanks to user Kashif Hisam).

NSTimer class reference . NSRunLoop class reference

Informal discussion of run loops on cocoadev.com

Apple docs on run loop modes

One Comment