Using blocks as a flexible callback mechanism

September 13, 2009

Among the many numerous developer improvements in Snow Leopard is the addition of blocks to the Objective-C language (now at version 2.1). While working on some networking code using NSURLConnection I discovered that blocks provide a beautiful and flexible mechanism for implementing callbacks.

As a quick example, let’s say you want to initiate two GET requests to different websites. Perhaps the first is to post some form data and the other is just to download the website content. No matter the particular specifics, the idea is that for different types of NSURLConnection requests you will want to respond differently upon completion. For the first you might want to update the UI to show that the form data was posted. For the second you may want to display the site in a web view. To accomplish this you would traditionally need to keep an array around to hold not only the NSURLConnection instances themselves, but some kind of associated identifier for each one. Otherwise, when your delegate methods fire you will have no idea which action initiated the request in the first place – and thusly you wouldn’t necessarily know what to do next after the connection is finished loading.

The following is a bit of untested pseudo-code (a rough approximation if you will) of one of the traditional methods of handling this:

- (void)performMyFirstRequest
{
  NSURLRequest *request = [NSURLRequest requestWithURL:myFirstURL];
  NSURLConnection *connection = [NSURLConnection connectionWithRequest:request 
                                         delegate:self];
 
  [_connections addObject:[NSDictionary dictionaryWithObject:connection, 
       @"NSURLConnection", @"myFirstRequestIdentifier", @"identifier", nil]];
}
 
- (void)performMySecondRequest
{
  NSURLRequest *request = [NSURLRequest requestWithURL:mySecondURL];
  NSURLConnection *connection = [NSURLConnection connectionWithRequest:request 
                                         delegate:self];    
  [_connections addObject:[NSDictionary dictionaryWithObject:connection, 
       @"NSURLConnection", @"someOtherIdentifierHere", @"identifier", nil]];
}
 
- (void)connection:(NSURLConnection *)connection 
           didReceiveResponse:(NSURLResponse *)response
{
  NSDictionary *connectionInfo = [self _infoForConnection:connection];
  NSString *identifier = [connectionInfo objectForKey:@"identifier"];
 
  if ([identifier isEqualToString:@"myFirstRequestIdentifier"])
    NSLog( @"The first request was successful!" ); // do something else
  else if ([identifier isEqualToString:@"someOtherIdentifierHere"])
    NSLog( @"The second request was successful!" );
 
  // Remember to remove the connectionInfo dictionary from our _connections 
  // ivar once the connection is finished loading
}
 
- (NSDictionary *)_infoForConnection:(NSURLConnection *)connection
{
  for (NSDictionary *connectionInfo in _connections)
  {
    if ([connectionInfo objectForKey:@"NSURLConnection"] == connection)
      return connectionInfo;
  }
 
  return nil;
}

In short, you will have to have a bunch of conditional if-else statements in order to properly respond to each different connection’s completion. The alternative approach is to write a URLConnectionManager class based on blocks:

// URLConnectionManager.h
 
@interface URLConnectionManager : NSObject 
 
- (void)performRequest:(NSURLRequest *)request 
         responseBlock:(void (^)(NSURLResponse *response))responseBlock;
 
@end
 
// URLConnectionManager.m
 
- (void)performRequest:(NSURLRequest *)request 
         responseBlock:(void (^)(NSURLResponse *response))responseBlock
{
  NSURLConnection *connection = [NSURLConnection connectionWithRequest:request 
                                          delegate:self];
  [connection associateValue:(id)responseBlock withKey:@"responseBlock"];
}
 
- (void)connection:(NSURLConnection *)connection 
           didReceiveResponse:(NSURLResponse *)response
{
  void (^responseBlock)(NSURLResponse *) = [connection associatedValueForKey:
                                                   @"responseBlock"];
  responseBlock( response );
}

This code also takes advantage of associated objects, another new language feature that allows us to avoid having to keep an array of connection info dictionaries around to store the associated responseBlock reference. (I am using an elegant category wrapper of the C API written by Andy Matuschak.)

Using this approach, the two connection requests can be implemented as follows:

// The first request
NSURLRequest *request = [NSURLRequest requestWithURL:myFirstURL];
 
[connectionManager performRequest:request 
                    responseBlock:^(NSURLResponse *response) {
  NSLog( @"Received a response from myFirstURL: %ld %@", 
                [(NSHTTPURLResponse *)response statusCode], response );
  // Perform whatever other specific magic you need for this response!
}];
 
// The second request
NSURLRequest *request = [NSURLRequest requestWithURL:mySecondURL];
 
[connectionManager performRequest:request 
                    responseBlock:^(NSURLResponse *response) {
  NSLog( @"Received a response from mySecondURL: %ld %@",
                [(NSHTTPURLResponse *)response statusCode], response );
  // Perform whatever other specific magic you need for this response!
}];

Note that there are no more if-else statements to deal with and even though the requests are being performed asynchronously, you can write a piece of code to deal with the response in the same spot that you make the initial connection invocation. This results in a much cleaner and more readable codebase.

Browse and/or download the full Xcode sample project on GitHub

Previous:

Next: