Delayed Messaging
Volume Number: 14
Issue Number: 12
Column Tag: Yellow Box
by Mike Morton
Benefits of Procrastination: Delayed Messaging using the
Foundation Kit
When you send an Objective-C message to an object, you expect that object to receive
the message and process it immediately. Right? Maybe not...
The Foundation Kit provides a way to post messages and have them delivered later,
through the method performSelector:withObject:afterDelay:. This method is part of the
NSObject class, from which most other classes descend. In this article, we'll discuss
not only how you use this method, but give some examples of how delayed messaging
solves some difficult problems.
If you've just begun working with Foundation Kit and AppKit, don't worry - we'll
explain things step-by-step. In each of the four examples below, we'll describe the
problem, show the solution working in the application, and then review the relevant
parts of the code. But first, let's look at the method itself.
How to Send a Delayed Message
The method which lets you send delayed messages to any NSObject is declared like this:
- (void) performSelector
:(SEL) aSelector
withObject :(id) anArgument
afterDelay :(NSTimeInterval) delay;
This method takes three arguments:
aSelector specifies the message to send. A selector is sort of the "name" of a method, a
very distant cousin of C's function pointers. (In some implementations, a selector is
just a "char *" pointer to a unique string value, and this can be useful in debugging,
but you should never depend on it being so.) Unlike function pointers, selectors have
their own data type, SEL, and you can refer to them in Objective-C code using the
@selector(...) construct. For example, if you'd like to send the message setFoo:, you
can specify the selector for that message with @selector(setFoo:).
anArgument is an optional object to send with the message. If you don't want to pass
anything, you can pass nil. But keep in mind that if you do want to pass something, it
must be an object - not an integer or other C type. (In theory, the method specified by
aSelector should take a single argument, but no-argument methods seem to work fine.
Of course, two-argument methods don't work, because there's no way to pass a second
argument with this API.) delay is the number of seconds to delay before sending the
message. [If you used the method like this one in earlier releases of Foundation, note
that the delay is no longer expressed in milliseconds.] If the program is busy, the
message may take longer before being delivered, but it will never get delivered early.
Using this method, you can send any one-argument message with a delay. For example,
if you have an object printer which implements the method printString:, you can have
it receive a printString: message after a delay of ten seconds with this code:
[printer performSelector :@selector(printString:)
withObject :@"Hello, world!
afterDelay :10.0];
This is almost the same as messaging the object directly with [printer printString
:@"Hello, world!"], except that the message gets delivered later, not "while you wait".
Also, of course, if printString: returns a value, you can't get that value when using
delayed messaging, because you want to continue before the message even gets received.
(Modern CPUs are fast, but still don't support time-travel.)
If you change your mind about a delayed message you sent earlier, you can cancel it,
using an NSObject class method:
+ (void) cancelPreviousPerformRequestsWithTarget
:(id) aTarget
selector :(SEL) aSelector
object :(id) anArgument;
That's the "how" of delayed messaging. But why would you want to use it. Each of the
four examples shows one reason why - let's take a look in more detail. The source code
for each of these demos is available online at ftp://ftp.mactech.com.
Example #1: Are They Going to Click Again?
Suppose you're implementing a web browser (just to give Netscape and Microsoft some
competition). If the user clicks on a link, the browser displays the new page in the
same window. But if they double-click the link, you want to open a new window.
What should your program do on that first click? You can't display the new page
(because if a second click shows up, that means the user didn't want to change this
window's display). But you also can't ignore the first click (because if no second click
arrives, you want to display the new page).
Try it: The demo doesn't browse the web, but it does show an example of not acting on
every click. Quickly click on the red rectangle one or more times; it changes color
with every click. Now check "Wait for clicks to stop before redisplaying", and it will
change color only after you finish clicking.
Figure 1. Acting on multiple clicks
This part of the demo uses a custom view named ColorView, a subclass of the AppKit's
important NSView class. The two instance variables for the class remember (1) what
color it currently draws, and (2) how quickly it redraws. The class interface, from
ColorView.h, begins like this:
Listing 1: Start of ColorView class interface
@interface ColorView : NSView
NSColor *color; // color we fill with
BOOL delaysDisplay; // YES => wait to redraw
}
The header file also lists access methods, methods which get and set the instance
variables, but these aren't shown in Listing 1.
The implementation, in ColorView.m, overrides some methods from NSView -
initWithFrame:, which all views use to initialize; drawRect:, which displays the
view's contents; and mouseDown:, which handles mouse clicks. The latter two are more
important.
The drawRect: method (Listing 2) is simple. It sends a set message to the view's
current color, so that subsequent PostScript drawing will use that color, and calls a
PostScript function to fill in the rectangle.
Listing 2: ColorView's drawRect: method
- (void)drawRect // redraw contents of...
:(NSRect) aRect; // INPUT: ...this rect
[[self color] set]; // paint with this color
PSrectfill (aRect.origin.x, aRect.origin.y,
aRect.size.width, aRect.size.height);
}
The mouseDown: method (Listing 3) is also short, but does a lot more. It updates the
view's color, based on the event's click count - the NSEvent method clickCount returns
1 for a click, 2 for a double-click, etc.
Listing 3: ColorView's mouseDown: method
- (void) mouseDown
:(NSEvent *) theEvent;
// Update our color: red for a single-click,
// orange for a double-click, etc.
[self _setColorAtIndex :[theEvent clickCount]-1];
}
When mouseDown: sends _setColorAtIndex:, that method winds up calling setColor:,
which is where things start to get interesting. When you give the ColorView a new
color, it will redraw itself, but it may not do so immediately. After it remembers the
new color, it checks "if ([self delaysDisplay])". If it doesn't delay displaying, it sends
"[self setNeedsDisplay :YES]" - that's how you ask any NSView to redraw.
But if you click the checkbox "Wait for clicks to stop before redisplaying", the
checkbox sends takeDelaysDisplayFrom: to the ColorView, which will take a new value
of delaysDisplay from the checkbox.
And when [self delaysDisplay] returns YES, the ColorView knows that it shouldn't
redraw, but it also that it shouldn't forget about the color change, since it needs to
redraw at some point. So it does the code in Listing 4: first, it removes any pending
redraw requests, then it sends a _setNeedsDisplayYes message, but delaying the
delivery. (Why doesn't it send a delayed setNeedsDisplay:, with an argument of YES?
Because the argument of a delayed message must be an object.)
Listing 4: ColorView's delaying of redraw
// Cancel any pending message...
[NSObject cancelPreviousPerformRequestsWithTarget
:self
selector :@selector(_setNeedsDisplayYes)
object :nil];
// ...and queue a new one
[self performSelector
:@selector(_setNeedsDisplayYes)
withObject :nil
afterDelay :REDRAW_DELAY];
}