Close
Glad You're Ready. Let's Get Started!

Let us know how we can contact you.

Thank you!

We'll respond shortly.

LABS
Wrapping Objective-C Delegates with Blocks, Part 1

Since adding blocks to Objective-C, Apple has designed more and more of their APIs to use them as the preferred callback mechanism over delegation—and for good reason. Blocks are (relatively) easy to use, lightweight, they play well with GCD, and most importantly for this post, they put the callback code right where it has the most context, thus making your code easier to read and reason about.

Many Apple APIs still use delegation, however, where blocks might make more sense; you can see this most often in classes designed before blocks became available. In this post and its follow-up, we’re going to look at two ways in which we can wrap an existing Apple API to use blocks instead of delegates.

UIAlertView will serve as our Guinea pig. It’s nearly ubiquitous in iOS apps, and it’s pretty easy to use. Nevertheless, it relies on a delegate to handle the user event of tapping one of its buttons, and this leads to clunky-feeling code:


@interface ViewController () <UIAlertViewDelegate>
@property (nonatomic, weak, readwrite) IBOutlet UIButton *button;
@property (nonatomic, weak, readwrite) IBOutlet UILabel *label;
@end

@implementation ViewController

- (IBAction)buttonTapped:(id)sender {
    UIAlertView *alertView = [[UIAlertView alloc]
                      initWithTitle:@"Alert!"
                            message:@"Tap a response"
                           delegate:self
                  cancelButtonTitle:@"Cancel"
                  otherButtonTitles:@"Choice 1", @"Choice 2", nil];
    [alertView show];
}

//
// Lots and lots of other code
//

#pragma mark - <UIAlertViewDelegate>

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
  if (buttonIndex == 0) {
    self.label.text = @"You tapped 'Cancel'";
  } else {
    self.label.text = [NSString stringWithFormat:@"You tapped 'Choice %d'", buttonIndex];
  }
}

@end

Using the Apple-provided API, the UIAlertView is instantiated with its buttons and shown immediately—and when the user taps on one of those buttons, the code that handles the event is in a completely different method, perhaps separated by many lines, perhaps even so many that it’s offscreen.

Using blocks, we might imagine the API looking something like this:


@interface APBAlertView : UIAlertView

- (instancetype)initWithTitle:(NSString *)title
    message:(NSString *)message
    cancelButtonTitle:(NSString *)cancelButtonTitle
    otherButtonTitles:(NSArray *)otherButtonTitles
    cancelHandler:(void (^)(void))cancelHandler
    confirmationHandler:(void (^)(NSInteger otherButtonIndex))confirmationHandler;

@end

Here, the code that handles the user event is right in the same place as the alert view’s initialization and display code itself. This results in much more elegant and readable code that reveals the programmer’s intent more obviously.

So let’s build this new APBAlertView and see how we are able to derive the benefits of using block-based callbacks.

I’ve created a project called AlertMe, which you can download from GitHub, and if you want, you can follow along by checking out the commit with the tag “Stock UIAlertView”. As you can see, the project is very simple: a UIButton triggers a UIAlertView, and tapping one of the buttons updates a label. Open up ViewControllerSpec.mm to see the tests that drive this feature (this project uses Cedar for testing). Amazingly, we won’t have to change them at all—APBAlertView will be a drop-in replacement for UIAlertView, and because our controller-level tests are behavioral, everything should work the same.

So we’ll create APBAlertView as a subclass of UIAlertView, and while we’re at it, we’ll create a spec file. APBAlertView will act as it’s own delegate, and it will handle the delegate events by calling the appropriate completion blocks, as the tests imply:


describe(@"APBAlertView", ^{
  __block APBAlertView *alertView;
  __block BOOL cancelled;
  __block BOOL button1Pressed;
  __block BOOL button2Pressed;

  beforeEach(^{
    cancelled = NO;
    button1Pressed = NO;
    button2Pressed = NO;

    alertView = [[APBAlertView alloc] initWithTitle:@"Title"
                           message:@"message"
                 cancelButtonTitle:@"Cancel"
                 otherButtonTitles:@[@"button1", @"button2"]
                     cancelHandler:^{
                       cancelled = YES;
                     }
              confirmationHandler:^(NSInteger otherButtonIndex){
                switch (otherButtonIndex) {
                  case 0: button1Pressed = YES;
                    break;
                  case 1: button2Pressed = YES;
                    break;
                  default:
                    break;
                }
              }];
  });

  it(@"should create the alert view", ^{
    alertView.title should equal(@"Title");
    alertView.message should equal(@"message");
    [alertView buttonTitleAtIndex:alertView.cancelButtonIndex] should equal(@"Cancel");
    [alertView buttonTitleAtIndex:1] should equal(@"button1");
    [alertView buttonTitleAtIndex:2] should equal(@"button2");
    alertView.delegate should be_same_instance_as(alertView);
  });

  sharedExamplesFor(@"releasing the completion handlers", ^(NSDictionary *sharedContext) {
    beforeEach(^{
      cancelled = NO;
      button1Pressed = NO;
    });

    it(@"should release the cancel handler", ^{
      [alertView dismissWithClickedButtonIndex:alertView.cancelButtonIndex animated:NO];
    cancelled should_not be_truthy;
    });

    it(@"should release the confirmationHandler", ^{
      [alertView dismissWithClickedButtonIndex:1 animated:NO];
      button1Pressed should_not be_truthy;
    });
  });

  describe(@"pressing buttons", ^{
    context(@"cancel button", ^{
      beforeEach(^{
        [alertView dismissWithClickedButtonIndex:alertView.cancelButtonIndex
                                        animated:NO];
      });

      it(@"should call the cancel handler", ^{
        cancelled should be_truthy;
      });

      itShouldBehaveLike(@"releasing the completion handlers");
    });

    context(@"button1", ^{
      beforeEach(^{
        [alertView dismissWithClickedButtonIndex:1 animated:NO];
      });

      it(@"should call the confirmation handler with button 1", ^{
        button1Pressed should be_truthy;
        button2Pressed should_not be_truthy;
      });

      itShouldBehaveLike(@"releasing the completion handlers");
    });

    context(@"button2", ^{
      beforeEach(^{
        [alertView dismissWithClickedButtonIndex:2 animated:NO];
      });

      it(@"should call the confirmation handler with button 2", ^{
        button2Pressed should be_truthy;
        button1Pressed should_not be_truthy;
      });

      itShouldBehaveLike(@"releasing the completion handlers");
    });
  });
});

Getting the tests to pass requires relatively straightforward code:


@interface APBAlertView () <UIAlertViewDelegate>
@property (nonatomic, copy) void (^confirmationHandler)(NSInteger otherButtonIndex);
@property (nonatomic, copy) void (^cancelHandler)(void);
@end

@implementation APBAlertView

- (instancetype)initWithTitle:(NSString *)title
                      message:(NSString *)message
            cancelButtonTitle:(NSString *)cancelButtonTitle
            otherButtonTitles:(NSArray *)otherButtonTitles
                cancelHandler:(void (^)(void))cancelHandler
          confirmationHandler:(void (^)(NSInteger otherButtonIndex))confirmationHandler {
  if (self = [super initWithTitle:title
                          message:message
                         delegate:self
                cancelButtonTitle:cancelButtonTitle
                otherButtonTitles:nil]) {
    for (NSString *buttonTitle in otherButtonTitles) {
      [self addButtonWithTitle:buttonTitle];
    }
    self.cancelHandler = cancelHandler;
    self.confirmationHandler = confirmationHandler;
  }
  return self;
}

#pragma mark - <UIAlertViewDelegate>

- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {
  if (buttonIndex == alertView.cancelButtonIndex) {
    if (self.cancelHandler) {
      self.cancelHandler();
    }
  } else {
    if (self.confirmationHandler) {
      NSInteger indexOffset = (alertView.cancelButtonIndex==-1) ? 0 : 1;
      self.confirmationHandler(buttonIndex - indexOffset);
    }
  }
  self.cancelHandler = nil;
  self.confirmationHandler = nil;
}

@end

And that’s it! From here we can go back to our view controller and re-implement the alert view:


@implementation ViewController

- (IBAction)buttonTapped:(id)sender {
  APBAlertView *alertView = [[APBAlertView alloc]
                           initWithTitle:@"Alert!"
                                 message:@"Tap a response"
                       cancelButtonTitle:@"Cancel"
                       otherButtonTitles:@[@"Choice 1", @"Choice 2"]
                           cancelHandler:^{
                             self.label.text = @"You tapped 'Cancel'";
                           }
                     confirmationHandler:^(NSInteger otherButtonIndex) {
                       NSInteger choice = otherButtonIndex + 1;
                       self.label.text = [NSString stringWithFormat:@"You tapped 'Choice %d'", choice];
                     }];
  [alertView show];
}

@end

No delegate necessary—everything is contained in the same place. The controller code is shorter, easier to read, and easier to understand. Win!

In Part 2, we’ll see another implementation of APBAlertView that uses promises to provide the callback handling for us.

Comments
Post a Comment

Your Information (Name required. Email address will not be displayed with comment.)

* Copy This Password *

* Type Or Paste Password Here *