iOS Programming Recipe 22: Simplify UIAlertView with Blocks

Assumptions

Getting Started

Have you ever found yourself wanting to present an alertView to the user to get simple yes or no feedback? If you’re anything like me you were very annoyed that you were required to implement the UIAlertView delegate protocol just to handle this simple task.

Is There A Better Way?

To answer that first we must examine the problem and and specify an ideal solution.

The problem

Getting input from the user is an asynchronous interaction for which UIAlertView has chosen the delegate pattern to handle.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)presentAlertView
{
    UIAlertView *alertView = [[UIAlertView alloc] init…
    alertView.delegate = self;
    [alertView show];
}

#pragma mark - UIAlertViewDelegate

// Called when a button is clicked. The view will be automatically dismissed after this call returns
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    // handle the button click
}

Ideal Solution

Present the alert to the user and specify what to do with the response directly in line with the alert. This way everything to do with the alert can be found in scope and the code reads intuitively.

The delegate pattern doesn’t allow us to define what to do with the users response to the alert in the same scope that the alert was presented making the code unnecessarily complex. Fortunately, there is another common pattern (maybe slightly more advanced) for handling asynchronous actions such as the alertView case.

Blocks!

If UIAlertView was able to take a block parameter to excite when the user makes a selection then we could specify what to do with the response in scope with presenting the alert! That would look similar to the following…


1
2
3
4
5
6
7
- (void)presentAlertView
{
    UIAlertView *alertView = [[UIAlertView alloc] init…
    [alertView showWithCompletion:^(UIAlertView *alertView, NSInteger buttonIndex) {
        // handle the button click
    }];
}

Wow! That is simple and much more readable than implementing the delegate and handling the response out of scope in some other place in the code.

Implementing Block Completion

Choosing The Right Solution

There are a few ways we could go about adding this functionality to UIAlertView.

  • Subclass UIAlertView
  • Create a completely new alert class that wraps an alertView
  • Extend UIAlertView with a category

Subclassing the alertView would work, but would then require everyone to remember some new class name and that is not ideal. After all, we are doing this to make the code simpler and easier to read, so this not an ideal solution. Creating a new wrapper class has the same issues and doesn’t introduce any real advantages in this situation. So that leaves us with creating a UIAlertView category, which seems fairly logical since we really only want to add one method to the alertView!

However, after thinking about the problem for a minute you may realize that you are going to need somewhere to store the completion block and categories don’t allow you to add instance variables… Fortunately, there is a really cool feature in the Objective-C runtime called associated objects that will allow us to get around this problem.

We have another problem with using a category for this implementation, who is going to call the completion block? UIAlertView uses a delegate system and someone is going to have to implement the delegate. To solve this issue we will use a private wrapper class that implements the UIAlertViewDelegate protocol and stores the completion block. So as it turns out we will use associated objects for storing this wrapper not the completion block, but it is the same concept.

Implementation
  • Let’s start by creating a basic single view application to test the alertView category that we are about to write.
  • Create a new UIAlertView category, we name ours NSCookbook. Xcode will enforce the proper naming convention creating both UIAlerView+NSCookbook.h and UIAlerView+NSCookbook.m
  • Open UIAlerView+NSCookbook.h and add the following interface as described above. All we are going to do is add a showWithCompletion: method.

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>

@interface UIAlertView (NSCookbook)

- (void)showWithCompletion:(void(^)(UIAlertView *alertView, NSInteger buttonIndex))completion;

@end
  • Next open UIAlerView+NSCookbook.m, we are going to first create a private class in order to implement the UIAlertViewDelegate protocol and store the completion block. We called this class NSCBAlertWrapper and put it at the top of UIAlerView+NSCookbook.m before implementing the category extension. It has one property defined in the header which is the completion block. It also conforms to the UIAlertViewDelegate protocol and implements the two action methods from said protocol and calls the completion block if it exists. Thats it!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#import <objc/runtime.h>
#import "UIAlertView+NSCookbook.h"

@interface NSCBAlertWrapper : NSObject

@property (copy) void(^completionBlock)(UIAlertView *alertView, NSInteger buttonIndex);

@end

@implementation NSCBAlertWrapper

#pragma mark - UIAlertViewDelegate

// Called when a button is clicked. The view will be automatically dismissed after this call returns
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
    if (self.completionBlock)
        self.completionBlock(alertView, buttonIndex);
}

// Called when we cancel a view (eg. the user clicks the Home button). This is not called when the user clicks the cancel button.
// If not defined in the delegate, we simulate a click in the cancel button
- (void)alertViewCancel:(UIAlertView *)alertView
{
    // Just simulate a cancel button click
    if (self.completionBlock)
        self.completionBlock(alertView, alertView.cancelButtonIndex);
}

@end

static const char kNSCBAlertWrapper;
@implementation UIAlertView (NSCookbook)

#pragma mark - Class Public

- (void)showWithCompletion:(void(^)(UIAlertView *alertView, NSInteger buttonIndex))completion
{
    NSCBAlertWrapper *alertWrapper = [[NSCBAlertWrapper alloc] init];
    alertWrapper.completionBlock = completion;
    self.delegate = alertWrapper;

    // Set the wrapper as an associated object
    objc_setAssociatedObject(self, &kNSCBAlertWrapper, alertWrapper, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    // Show the alert as normal
    [self show];
}

@end
  • After the wrapper class has been implemented, all we need now is the showWithCompletion: implementation. This requires the use of associated objects so we added an import for the objective-C runtime at the top. First, we create an instance of the wrapper class we just defined, set the completion block on it, and then set it as the alertView’s delegate. Then we make use of associated objects, which is just a fancy way of setting the wrapper as a property of the alertView (not exactly, but you can think of it that way). The associated object requires a key, so we also define a static const char for this purpose. We set the association policy for the object to OBJC_ASSOCIATION_RETAIN_NONATOMIC which will retain the wrapper and in ARC deallocate it with the alertView. Finally, we then call the basic show method on UIAlertView.
Finish The Sample App
  • In your single view sample app, open ViewController.xib and add a button in the center, title it something like show alert.
  • Create a IBAction for the button in ViewController.h called showAlert:, then link it to the button.

1
2
3
4
5
6
7
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController

- (IBAction)showAlert:(id)sender;

@end
  • Open ViewController.m and implement showAlert: using our new UIAlertView completion block API. Of course you will need to import UIAlertView+NSCookbook.h or whatever you named it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#import "ViewController.h"
#import "UIAlertView+NSCookbook.h"

@implementation ViewController

- (IBAction)showAlert:(id)sender
{
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"NSCBSampleAlertTitle", @"")
                                                        message:NSLocalizedString(@"NSCBSampleAlertMessage", @"")
                                                       delegate:nil
                                              cancelButtonTitle:NSLocalizedString(@"NSCBSampleAlertCancel", @"")
                                              otherButtonTitles:NSLocalizedString(@"NSCBSampleAlertOk", @""), nil];

    [alertView showWithCompletion:^(UIAlertView *alertView, NSInteger buttonIndex) {
        UIAlertView *responseAlert = [[UIAlertView alloc] initWithTitle:NSLocalizedString(@"NSCBSampleAlertTitle", @"")
                                                            message:[NSString stringWithFormat:NSLocalizedString(@"NSCBSampleAlertCallbackFormat", @""), buttonIndex ? NSLocalizedString(@"NSCBSampleAlertOk", @"") : NSLocalizedString(@"NSCBSampleAlertCancel", @"")]
                                                           delegate:nil
                                                  cancelButtonTitle:@"Ok"
                                                  otherButtonTitles:nil];
        [responseAlert showWithCompletion:NULL];
    }];
}

@end

We first create a new alertView with a title and a message. you can ignore the localized strings if you are not familiar with localization. Then we show the alert using the new showWithCompletion: method, in which we show another alert displaying which button the user clicked. Very simple, but would be much more of a pain without the new block based approach!

Here is our Localizable.strings file if you are interested


1
2
3
4
5
6
7
NSCBSampleAlertTitle = "Sample Alert";
NSCBSampleAlertMessage  = "This alert uses a block callback instead implementing the super annoying delegate protocol that would normally be required just to handle a simple button click in an alert such as this.";

NSCBSampleAlertCancel   = "Cancel";
NSCBSampleAlertOk       = "Ok";

NSCBSampleAlertCallbackFormat = "You selected %@";

The Final app should look something like this…


Final Thoughts

There are many different implementations of this type of blocks addition to UIAlertView already out there on the web, but the concept can and has been extended to many other tasks. For example the same approach could be used to extend UIGestureRecognizer to use block callbacks. As always please feel free to comment with any questions or concerns!

A few interesting links on some side topics in this tutorial

Associated Objects

Localization

Blocks

Full source code to this tutorial is available on our GitHub page.

Comments

  1. Awesome technique. Here is a blog post on how static variable could be replaced http://www.tuaw.com/2013/04/10/devjuice-better-objective-c-associated-objects/

    Your thoughts?

    • Thanks for sharing this! I had heard of using the selector as the associated object key, but hadn’t seen an example.

  2. “The delegate pattern doesn’t allow us to define what to do with the users response”

    In the sentence above, “users” is a plural, meaning more than one user. The phrase should really be “user’s response”.

    Since our code must compile, so should our English.

    Great tutorials by the way, I’m quite glad to have discovered them.

  3. Awesome just what I needed, when I needed it :)

  4. Something i don’t understand. You are using objc_setAssociatedObject to associate the instance of UIAlertView with a instance of the “private” class which ultimately calls the completion block. However, this method takes a custom key. How does UIAlertView know what this key is to use it? How does it use it at all?

    I tried removing this line of code but i got a exception as may be expected. Im just curious though that if your setting a association you would pull that association later and i don’t see that happening and i don’t know how UIAlertView could do it when we give it a custom key?

  5. Great stuff! Thank you very much. I looked at several options, but this seems to be the very cleanest. Nicely done!

  6. I keep getting ‘-[UIAlertView showWithCompletion:]: unrecognized selector sent to instance’ whenever using this. After importing the implementation file instead of the header, there are no more problems. Can you shed any light on this?

    • I’ll answer my own question: I accidentally added my new category to the wrong target, so taking care of that resolved everything.

Speak Your Mind

*

css.php
Privacy Policy