UIKit+Blocks

Showing an alert in iOS is pretty easy; a few lines of code and you're done. But things start to get complicated and messy when you want to add an action to a button. In this post I will show you how to tidy up your alerts (and action sheets and animations) using blocks. At the end I'll give you access to the code that we use here at Innovattic as a freebie!

As a reminder, this is how we show a simple alert with just some text and an OK button:

UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"My alert"  
                                                message:@"Hello World!"
                                               delegate:nil
                                      cancelButtonTitle:@"OK"
                                      otherButtonTitles:nil];
[alert show];

Wonderful! But look how messy it gets when we want to add an action to one of the buttons in the alert. You will have to start using the delegate:

@interface ViewController () <UIAlertViewDelegate>
@end

@implementation ViewController

- (IBAction)buttonTouched:(id)sender {
    UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"My complicated alert"
                                                    message:@"Perform some kind of action?"
                                                   delegate:self
                                          cancelButtonTitle:@"No"
                                          otherButtonTitles:@"Yes", nil];
    [alert show];
}

- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex == [alertView firstOtherButtonIndex]) {
        NSLog(@"Perform some kind of action");
    }
}

@end

While this does work, it's not very pretty. The most important reason being that the logic of your alert is no longer all in one place. This becomes especially apparent when you have multiple alerts that all use the same delegate; presumably your view controller. All the logic of which button was pressed will end up in your
alertView:clickedButtonAtIndex: method where you have to figure out which alert is currently showing, and which button belongs to the given buttonIndex. Yuck.

Blocks to the rescue!

Blocks. Mmmmm blocks... I love blocks. Together with dispatch queues they are probably the single greatest additions to ever have come to Objective-C. Apple has been steadily adding blocks to Foundation and UIKit, but mostly only the newer APIs have received this treatment. Especially some UIKit classes are screaming to have blocks functionality added to them; UIAlertView and UIActionSheet in particular because they are being used so often.

While Apple may add this in the future, we don't really want to wait for that. Luckily we don't have to. With a bit of effort we can extend UIAlertView and UIActionSheet with the use of Objective-C categories so that we can rewrite the previous code as:

UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"My easy alert"  
                                                message:@"Would you like to perform some kind of action?"
                                      cancelButtonTitle:@"No"
                                      otherButtonTitles:@"Yes", nil];
[alert setHandler:^(UIAlertView* alert, NSInteger buttonIndex) {
    NSLog(@"Perform some kind of action");
} forButtonAtIndex:[alert firstOtherButtonIndex]];
[alert show];

Things to note are:

  1. No custom subclass of UIAlertView. Everything is added using a category.
  2. The delegate argument is removed from the initializer.
  3. The logic of pressing the 'Yes' button is exactly where it should be: the same place where you created the alert.

We can use a similar approach for UIActionSheet:

UIActionSheet* sheet = [[UIActionSheet alloc] initWithTitle:@"My easy action sheet"  
                                          cancelButtonTitle:@"Cancel"
                                     destructiveButtonTitle:nil
                                          otherButtonTitles:@"Action!", nil];
[sheet setHandler:^(UIActionSheet* sheet, NSInteger buttonIndex) {
    NSLog(@"Perform some kind of action");
} forButtonAtIndex:[sheet firstOtherButtonIndex]];
[sheet showInView:[self view]];

And as a bonus also for CAAnimation!

CAAnimation* animation = [CAAnimation animation];  
[animation setAnimationDidStartHandler:^(CAAnimation* animation) {
    NSLog(@"Animation did start");
}];
[animation setAnimationDidStopHandler:^(CAAnimation* animation, BOOL finished) {
    NSLog(@"Animation did stop");
}];

But how...?

You can find the exact implementation details by studying the code (see the last paragraph of this post), but I will briefly explain two tricks that I used to get this to work.

The downside of using an Objective-C category instead of a custom subclass is that it's not possible to add your own instance variables to the existing class, so you need to find another way to store the blocks. I have used two different methods; one for UIAlertView and UIActionSeet, the other for CAAnimation.

UIAlertView and UIActionSheet

For UIAlertView and UIActionSheet I exploited the fact that they are subclasses of UIView and so it is possible to add subviews to them. When instantiating an alert view with the blocks initializer, an instance of a special UIView subclass with size 0 x 0 is added to the view and it is set as the delegate of the alert view. All methods for adding the blocks are then forwarded to this special view which stores them in its instance variables. This way the lifetime of the blocks are tied to the lifetime of the alert view. When the alert view is deallocated, so are all of its subviews and hence also the attached blocks.

You have to be careful not to have strong references to the alert views themselves inside of your blocks though! This would lead to retain cycles (assuming that you're using ARC): the alert view has a strong reference to its special subview, which has strong references to the blocks, which in turn have a strong reference to the alert view. The alert view would never be deallocated in this case, along with any other references you have kept inside your blocks. Luckily, the alert view passes a reference to itself along with all the blocks (as you can see in the sample code) so you really don't have to keep a reference to it yourself.

CAAnimation

The CAAnimation (and also CALayer) class is a so-called 'key-value coding compliant container class'. What this comes down to is that you can add arbitrary values to its instances by calling setValue:forKey: for any key. For more in-depth information about this you can check out Apple's documentation. This feature is extremely useful for all sorts of things when working with Core Animation, but sadly it's not widely known because it's mentioned in only a few places in the documentation.

The blocks for the animations are therefor simply stored and retrieved by calling setValue:forKey and valueForKey: with unique keys. The animation's delegate is set to a special singleton class which simply retrieves the correct block and calls it upon recieving the animationDidStart: and animationDidStop:finished: delegate messages.

Sweet! How do I get it?

Here at Innovattic we have a library full of this kind of goodies that we include in most of our projects. The source files for these 3 categories can be found at https://github.com/Innovattic/UIKit-Blocks. They are released into the public domain, so you are free to use them however you like.

comments powered by Disqus