Better View Controller callbacks with Blocks

5 Jan

So you’re working on an app and it needs to present a UIPopoverController to give your user some choices. Maybe your app provides a popover for the user to pick their height. Whenever the user picks a new height in the popover this selection needs to be communicated back to your main UIViewController so it can update it’s interface.

Easy, just make a custom UIViewController for the height picker and expose a delegate @protocol. Then, whenever the user picks a new height, the custom HeightViewController will call the delegate’s selection handler.

Here’s an example of what our HeightViewController interface might look like

@import UIKit; @class HeightViewController; @protocol HeightViewControllerDelegate <NSObject> - (void)heightViewController:(HeightViewController *)heightViewController didSelectHeight:(NSNumber *)selectedHeight; @end @interface HeightViewController : UIViewController @property (nonatomic, strong) NSNumber *height; @property (nonatomic, weak) id<HeightViewControllerDelegate> delegate; @end

And the interesting part of it’s @implementation

- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
    if ([self.delegate respondsToSelector:@selector(heightViewController:didSelectHeight:)]) {
        NSNumber *height = self.pickerArray[row];
        [self.delegate heightViewController:self didSelectHeight:height];
    }
}

Then in our main view controller…

- (void)heightViewController:(HeightViewController *)heightViewController didSelectHeight:(NSNumber *)selectedHeight {
    self.height = selectedHeight;
}

This is all working great, one simple UIViewController subclass and you’ve exposed just what’s needed to update the main view.

Then the next day comes…now you need to allow the user to pick their weight. “No problem!” you say. So you merrily create another UIViewController subclass called WeightViewController and add another delegate callback to your main view controller.

Then another day passes…and you need to expose a department picker, a title picker and date picker, or two, or three, or four, before you know it you’ve made a dozen custom view controllers and littered your main view controller with code to configure and handle all the selection callbacks. Things are starting to get out of hand.

There’s a better way.

BLOCKS. Rather than exposing a @protocol for each picker let’s try using BLOCKS for our callbacks.

Take our custom HeightViewController.h for example.

@import UIKit;

typedef void (^HeightViewControllerSelectionHandler)(NSNumber *selectedHeight);

@interface HeightViewController : UIViewController

@property (nonatomic, strong) NSNumber *height;
@property (readwrite, copy) HeightViewControllerSelectionHandler selectionHandler;

@end

Pretty simple huh? We’ve removed the @protocol and delegate code. Now we just expose a selectionHandlerblock property that our main view controller will use to update itself.

Now lets look at how that changes our HeightViewController.m @implementation

- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
    // A quick check to see if we have a selectionHandler block
    if (self.selectionHandler) {
        NSNumber *height = self.pickerArray[row];
        self.selectionHandler(height); // it's that simple to use the callback block
    }
}

Finally in our main view controller. Instead of having separate code to create, configure and response to the delegate callback we handle it all during the creation of the HeightViewController

// Alloc and init the height picker view controller
HeightViewController *heightViewController = [[HeightViewController alloc] initWithNibName:@"HeightViewController" bundle:[NSBundle mainBundle]];

// configure it's initial height
heightViewController.height = self.height;

// set our block callback
heightViewController.selectionHandler = ^(NSNumber *selectedHeight){
    self.height = selectedHeight;
};

// Present the height picker popover
self.popover.contentViewController = heightViewController;
[self.popover presentPopoverFromRect:rect inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];

No more delegate callbacks littered all throughout your main view controller. As an added bonus we get to keep the callback code with the alloc / init code, so our code’s sequence of events is clearer.

I’m not saying you should always use blocks instead of delegates, but in this case it seems like a good choice.