Big Nerd Ranch Advanced Mac OS X Programming: Blocks
One shortcoming of C and Objective-C is code that performs a particular task becomes scattered around the code base. Say you are performing an operation on a C array of People structs and you need to sort the list by last name. Luckily, there is the library function qsort() to sort the array. qsort() is well-optimized and does its job well, but it needs some help. It does not know how items are compared to each other so that they end up in the proper sorted order.
... qsort (peopleArray, count, sizeof(Person), personNameCompare); ... int personNameCompare (const void *thing1, const void *thing2) { Person *p1 = (Person *) thing1; Person *p2 = (Person *) thing2; return strcmp(p1->lastName, p2->lastName); } // compare
The compare function is not close to the invocation of qsort(). If you want to know exactly what the comparison is doing while you are reading or maintaining the code, you will need to find personNameCompare(), perhaps look at surrounding code, and then return to your original place.
Similarly, in Cocoa you frequently initiate some process that is handled in a place distant from the initial call, kicking asynchronous work that is completed in callbacks. Say the user pushes a button to load an image, and an NSOpenPanel is displayed:
- (IBAction) startImageLoad: (id) sender { NSOpenPanel *panel = [NSOpenPanel openPanel]; ... [panel beginSheetForDirectory: nil ... modalDelegate: self didEndSelector: @selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo: nil]; } // startImageLoad
The callback method is somewhere else in the source file:
- (void) openPanelDidEnd: (NSOpenPanel *) sheet returnCode: (int) code contextInfo: (void *) context { if (code == NSOKButton) { NSArray *filenames = [sheet filenames]; // Do stuff with filenames. } } // openPanelDidEnd
The initiation of the open panel is separated both in time and space from the handling of the open panel's results. You need to find a place to hide any data that needs to be communicated from one place to another, such as in a context parameter, instance variable, or global variable.
Wouldn't it be nice to have these auxiliary chunks of code near where they are being invoked? That way you can take in the entirety of an operation in a single screenful of code without having to hop around your codebase.
Blocks are a new feature added by Apple to the C family of languages, available in Mac OS X 10.6 and later and iOS 4.0 and later. Blocks allow you to put code that does work on behalf of other code in one place.
The qsort() function call would look this when expressed with blocks:
qsort_b (elements, count, sizeof(element), ^(const void *thing1, const void *thing2) { Person *p1 = (Person *) thing1; Person *p2 = (Person *) thing2; return strcmp(p1->lastName, p2->lastName); } );
The open panel code would look something like this:
[panel beginSheetModalForWindow: window // ... completionHandler: ^(NSInteger result) { if (result == NSOKButton) { NSArray *fileNames = [sheet filenames]; // do stuff with fileNames } } ];
Block Syntax
A block is simply a piece of inline code. Here is an NSBlockOperation that logs a line of text when the operation is scheduled to run.
NSBlockOperation *blockop; blockop = [NSBlockOperation blockOperationWithBlock: ^{ NSLog (@"The operation block was invoked"); }];
The block is introduced by the caret with the code of the block surrounded by braces. Bill Bumgarner from Apple said that the caret was chosen because "it is the only unary operator that cannot be overloaded in C++, and the snowman is out because we can't use unicode."
The code inside of the block is not executed at the same time as the function or method call that contains the block. The NSLog above will not be executed when the NSBlockOperation has been created; instead, it will be called at a later time when the operation is finally run.
Blocks can take arguments. NSArray's -enumerateObjectsUsingBlock: will enumerate all objects in the array, invoking the block for each one. The block takes three parameters: the object to look at, the index of the object in the array, and a stop pointer to a BOOL. Setting *stop to YES will cause the iteration to cease before the array has been exhausted:
NSArray *array = [NSArray arrayWithObjects: @"hi", @"bork", @"badger", @"greeble", @"badgerific", nil]; [array enumerateObjectsUsingBlock: ^(id object, NSUInteger index, BOOL *stop) { NSLog (@"object at index %d is %@", index, object); }];
will print:
object at index 0 is hi object at index 1 is bork object at index 2 is badger object at index 3 is greeble object at index 4 is badgerific