Blocks and block lists
When I first heard about blocks, I was horrified. All the un-clean code structure of huge ten-page functions, plus the loss of self-documenting function names. Why would someone want anonymous functions? And even worse, anonymous functions that can cause retain circles because they automatically retain objects you reference, because they have access to the scope of the surrounding function, like nested functions?
Then, at one NSConference, someone pointed out that they are really good for making asynchronous code look more synchronous. Who would have thought that blocks, carefully used, could actually improve readability of code?
Imagine you are using an NSURLConnection to retrieve a JSON file from a server and extract information from it. In synchronous pseudocode:
-(void) showInfo { NSString* myJSON = [NSString stringWithContentsOfURL: @"http://example.com/data.json"]; if( myJSON == nil ) { [self reportError]; return; } NSString* userText = [self prettyPrintJSON: myJSON]; [self showPanelWithText: userText button: @"OK"]; }
Minimal code for doing this asynchronously and avoid blocking the user interface and spinning the beach ball when the connection is slow includes creating an NSURLConnection, setting yourself as its delegate then displaying any error you get from one delegate method, or when the request completes and a second is called, to actually process the request. So the above code turns into pseudocode:
-(void) showInfo { self.connection = [[NSURLConnection alloc] initWithURL: @"http://example.com/data.json"]; myConnection.delegate = self; [myConnection start]; } -(void) connection: (NSURLConnection*)sender didFinishLoading: (NSString*)myJSON { NSString* userText = [self prettyPrintJSON: myJSON]; [self showPanelWithText: userText button: @"OK"]; self.connection = nil; } -(void) connectionDidFailLoading: (NSURLConnection*)sender { [self reportError]; }
Notice how 5 straightforward lines have turned into a spaghetti of three methods? Now imagine you had to chain several requests. You’d have to create separate delegate objects for each request, or key off the ‘sender’ parameter and handle the OK case for all requests together in one delegate method, and the error cases together in another.
Now how does that look with blocks (still pseudocode)?
-(void) showInfo { [NSURLConnection sendAsynchronousRequest: @"http://example.com/data.json" completionHandler: ^( NSURLRequest req, NSString* myJSON, NSError* error ) { if( error ) { [self reportError]; return; } NSString * userText = [self prettyPrintJSON: myJSON]; [self showPanelWithText: userText button: @"OK"]; }]; }
Shockingly, that almost looks like our original, synchronous code. Now to be fair, if someone at Apple had added a delegate method to NSURLConnection that combined the error case and the success case like the above block does, the original example would be a tad simpler as well. But performing several requests in sequence would still split the actual requests from their replies, and group them by request or reply.
With blocks, on the other hand, two requests in sequence would look almost like the synchronous code would, with nested ifs:
-(void) showInfo { [NSURLConnection sendAsynchronousRequest: @"http://example.com/data.json" completionHandler: ^( NSURLRequest* req, NSString* myJSON, NSError* error ) { if( error != nil ) { [self reportError]; return; } NSString* userText = [self prettyPrintJSON: myJSON]; [self showPanelWithText: userText button: @"OK"]; [NSURLConnection sendAsynchronousRequest: @"http"//example.com/data2.json" completionHandler: ^( NSURLRequest* req2, NSString* myJSON2, NSError* error2 ) { if( error2 != nil ) { [self reportError]; return; } NSString * userText2 = [self prettyPrintJSON: myJSON2]; [self showPanelWithText: userText2 button: @"Done"]; }]; }]; }
You’re probably already seeing where our problem is, though. Since every block adds one level of brackets and depth, it becomes pretty hard to re-order requests. You’d have to cut the inner request out, then wrap it around the outer request so it only triggers the other request on success.
But here is where it comes in handy that blocks aren’t just nested functions. Blocks are actually real Objective-C objects. So, we can keep an array of blocks:
-(void) showInfo { NSMutableArray * steps = [NSMutableArray array]; [steps addObject: ^( NSMutableArray * steps ) { // First request start: [NSURLConnection sendAsynchronousRequest: @"http://example.com/data.json" completionHandler: ^( NSURLRequest* req, NSString* myJSON, NSError* error ) { if( error != nil ) { [self reportError]; return; } NSString* userText = [self prettyPrintJSON: myJSON]; [self showPanelWithText: userText button: @"OK"]; // First request end. [[steps[0] retain] autorelease]; // Make sure we don't go away. [steps removeObjectAtIndex: 0]; steps[0]( steps ); }]; }]; [steps addObject: ^( NSMutableArray * steps ) { // Second request start: [NSURLConnection sendAsynchronousRequest: @"http://example.com/data2.json" completionHandler: ^( NSURLRequest* req2, NSString* myJSON2, NSError* error2 ) { if( error2 != nil ) { [self reportError]; return; } NSString* userText2 = [self prettyPrintJSON: myJSON2]; [self showPanelWithText: userText2 button: @"OK"]; // First request end. [[steps[0] retain] autorelease]; // Make sure we don't go away. [steps removeObjectAtIndex: 0]; steps[0]( steps ); }]; }]; [steps addObject: ^( NSMutableArray * steps ) { // Done. }]; steps[0]( steps ); // Kick off execution }
So, we add the blocks to an array with 3 -addObject: calls, then at the bottom run the first block in the array. When each block is done, it removes itself from the array, and runs the next block in the array (except for the last block, which is simply there so the second-to-last block has someone after it to call — we could also just check whether the array is empty before trying to call the next block). This looks a little more complicated than it should, but if wrapped in an ObjC class with a -next method is very readable.
Now, if you want to re-order two requests, you simply re-order the -addObject: calls. Since none of the blocks care which block follows them, you don’t need to wrap anything around anything else.
You can now read all requests in sequence (hence I call these things block sequences), ignoring the spots at which control is given up to the system for a while, and re-arrange them as you please. Neat, huh?