The pogo stick of NSRunLoop

Quick intro to NSRunLoop

NSRunLoop

is the object that manages the big event loop in the sky.

Imagine you’re writing your own graphical OS.  A common way to build your control flow is with a loop like this:

while(!UserRequestedShutDown(userInput)) {
  userInput = GetUserInput();
  KeyApplicationHandleUserInput(userInput);
}

The loop continuously gathers and reacts to the user’s actions – this is the intuitive idea behind

NSRunLoop

, and the way any iPhone OS app works.

And how is this like a pogo stick, you might ask?

Well, if you could visualize the stack trace over time, it would look something like this:

Figure 1: Stack trace height vs. time + stick figure on pogo stick.

The stack trace is short before it calls any of your app’s code, and grows as your functions call each other, or other system code, until you’re done handling the particular message you’ve received, at which point the process is repeated for the next message.

Most of the time you don’t even have to worry about all this, but in some cases this architecture can require certain coding considerations.

The problem

For example, suppose you’re about to make a synchronous server call via

NSURLConnection

, but you want to present an activity indicator first to let the user know he/she has to wait for a second. It might be tempting to use code like this, but it won’t work:

  activityIndicator = [[[UIActivityIndicatorView alloc]
                        initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]
                       autorelease];
  [self.view addSubview:activityIndicator];
  [activityIndicator startAnimating];
  [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
  [activityIndicator stopAnimating];

Unfortunately, the user will never see the activity indicator with this code, because the visual appearance of the activity indicator is only scheduled to happen by the call to

[activityIndicator startAnimating]

– it won’t actually take effect until another (very soon-after) run loop in which the system begins the animation.

This pattern where visual changes are delayed until the next run loop is common because it’s more efficient.  Often we group together a lot of changes to the UI in a single place in code, and it makes sense to draw the end result after all changes are made rather than draw each change one at at time.  But this can have adverse effects, as the above example shows.

How can we fix this situation?

I’m going to present two solutions, both of which allow extra run loops to occur between pieces of your code.

Solution 1 – performSelector:withObject:afterDelay:

The safest fix for this is to break your code into pieces by writing a new method to be run in a near-future run loop.  This can be done using the

performSelector:withObject:afterDelay:

method, which is part of the

NSObject

class.

In general, this method runs the given selector (on the receiver) after the given delay measured in seconds (can be non-integral).  The given selector is assumed to take 0 or 1 arguments, where withObject: must be nil if 0 or the desired parameter to pass in, if 1.  You can pass in any number of seconds you want to afterDelay:, and this acts as a kind of one-off timer, which can be very handy.

To solve our problem, though, we can use this method with the delay set to 0.  This does not run the selector immediately – instead, it schedules it to be run as soon as possible after all other immediately pending run loops calls.  In our case, that’s exactly what we want.

Here’s an example of how we could use this to fix the above example:

- (void)loadPart1 {
  activityIndicator = [[[UIActivityIndicatorView alloc]
                        initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]
                       autorelease];
  [self.view addSubview:activityIndicator];
  [activityIndicator startAnimating];
  [self performSelector:@selector(loadPart2) withObject:nil afterDelay:0];
}

- (void)loadPart2 {
  [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
  [activityIndicator stopAnimating];
}

Solution 2 – runUntilDate:

If you really, really want to, it turns out you can avoid having to split up your code as in the last solution.

You can do this by directly telling the run loop to make one more run, within your own code, like this:

[[NSRunLoop currentLoop] runUntilDate:[NSDate date]];

And this gives us the nice handy solution:

- (void)loadInOnePart {
  activityIndicator = [[[UIActivityIndicatorView alloc]
                        initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]
                       autorelease];
  [self.view addSubview:activityIndicator];
  [activityIndicator startAnimating];
  [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
  [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
  [activityIndicator stopAnimating];
}

Which looks great until you consider the implications.  By using this approach, you’re effectively running a lot of things out of order.  It’s very reasonable to assume that run loops are run one after the other, with one finishing before the next begins.  By using this approaching, you’re scattering this assumption to the wind like the shattered dreams of a disillusioned soul in a dark, callous world.

Strictly the stuff of cavalier coding cowboys.

On the other hand, I’ve never seen a crash from this, so maybe it’s just one of those things that’s only scary if you think about it, like flying on a low-budget airline with a track record of many accidents.

One Comment