Adding Block Support to Existing Classes (Without Subclassing)

Blocks are great aren’t they? Amongst many other things, they allow you to put all your completion logic right next to where you call an asynchronous method. Apple has added block support for completion handlers in such APIs such as Core Animation, in fact I have already done a post on this previously. But what do you do, if the class you want to use is stuck in the past and still uses the older delegate style approach. This is where you have to write a method to handle all the delegate callbacks for a given class? Well thankfully there is a nice and clean way of adding block support to a class using a category.

I’ve previously written 2 posts on categories (which you can find here and here), and how they allow you to extend a class’s functionality by adding additional methods to it without subclassing it. The key word being methods (or indeed selectors if your getting technical), and not properties or instance variables. If you need to call a block after an asynchronous action has completed, you’re going to have to store it somewhere in the meantime, and that is the main focus of this post.

Take UIAlertView for example, you need to register for a callback when the user presses a button.

So you’d often have something like:

- (IBAction)showAlert:(id)sender {    
    UIAlertView *alertView = nil;

    alertView = [[UIAlertView alloc] initWithTitle:@"YES or NO"
    message:@"Select One" 
    delegate:self 
    cancelButtonTitle:@"NO" 
    otherButtonTitles:@"YES", nil];

    [alertView show];
    [alertView autorelease];
}

Then you would need to implement a method with the correct signature to handle it:

- (void)alertView:(UIAlertView *)alertView  didDismissWithButtonIndex:(NSInteger)buttonIndex {
    if(buttonIndex == 1) {
        // Do Something
    }
}

This may look fine in the above example, but in a real iOS application the callback method may have to handle lots of different alert views, and we still have the handler code separated from where we actually setup and show the alert view.

So how can we solve this?

We are going to add a completion handler to UIAlertView, which doesn’t return anything and takes the button index we get given by the UIAlertViews delegate method as a parameter. To keep things clean we will define our completion handler in our categories header: objc typedef void(^MCSMUIAlertViewCompletionHandler)(NSUInteger buttonIndex); So the first thing you will need to do is set the completion hander block on an alert view and store it for when we get the delegate callback. For this we will use the Objective-C runtime function: objc_setAssociatedObject.

The objc_setAssociatedObject function takes 4 parameters:

  1. The object that you want to add the association too, in this case the UIAlertView
  2. The unique key so you can retrieve the value later
  3. The value you want to associate, which in this case is the completion handler block
  4. The association policy, which tells the runtime wether this association retains, copies or assigns the value.

The best way to describe how this function works, is that it treats an object like and NSDictionary. It allows you to set a given value for a given key, but unlike NSDictionary you can specify if the value is assign, retained or copied.

To use this method you need to include the runtime header:

#import <objc/runtime.h>

And we will define our key as a constant to keep things tidy, and so we don’t make any typos later on:

NSString * const MCSMUIAlertViewCompletionHandlerKey = @"MCSMUIAlertViewCompletionHandlerKey";

Then in another category method on UIAlertView all you need to do is set the alert view as its own delegate, and store the block as an associated object:

- (void)MCSM_setCompletionHandler:(MCSMUIAlertViewCompletionHandler)handler{
self.delegate = (id<UIAlertViewDelegate>)self; 

objc_setAssociatedObject(
self, 
MCSMUIAlertViewCompletionHandlerKey,
handler,
OBJC_ASSOCIATION_COPY_NONATOMIC);

}

As the UIAlertView is now it own delegate, it will receive the alertView:didDismissWithButtonIndex: callback. In this method you will need to retrieve the completion handler block so you can call it with the button index argument. To do this you will need to use another Objective-C runtime function objc_getAssociatedObject

The objc_getAssociatedObject function takes 2 parameters:

  1. The object to retrieve the association from
  2. The unique key for the association

This will mean that you end up with a method looking like this:

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {    
    // Get the Handler
    MCSMUIAlertViewCompletionHandler handler = (MCSMUIAlertViewCompletionHandler)objc_getAssociatedObject(
    self,
    MCSMUIAlertViewCompletionHandlerKey);

    // If there is a handler call the handler
    if (handler) {
        handler(buttonIndex);
    }

    //Release the block by setting the associated object to nil
    objc_setAssociatedObject(
    self, 
    MCSMUIAlertViewCompletionHandlerKey, 
    nil, 
    OBJC_ASSOCIATION_COPY_NONATOMIC);
}

So now that we have added this category to UIAlertView, all we have to do is update the code that creates and shows the alert view:

- (IBAction)showAlert:(id)sender {
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"YES or NO"
    message:@"Select One" 
    delegate:self 
    cancelButtonTitle:@"NO" 
    otherButtonTitles:@"YES", nil];

    [alertView MCSM_setCompletionHandler:^(NSUInteger buttonIndex) {
        if (buttonIndex == 1) {
            // Do Something
        } 
    }];
    [alertView show];
    [alertView autorelease];
}

You can grab the code for this on Git Hub here and have a play with it yourself.