Search
Rich's Mad Rants
Powered by Squarespace

Entries in Ruby (1)

Sunday
May272012

RWAlertManager: a Ruby-inspired, fluent, block-based wrapper for UIAlertView

The Problem

I've always hated the way UIAlertView works. Oh, it's easy enough to display an alert. But, if you want to respond to any button presses, you have to filter everything through a delegate method. Besides the extra typing, and jumping back and forth to the header file, this also means I'm spreading my alert logic about. This is particularly bad, since I typically display an alert messages to the user to get a quick "A or B" answer. My code would be a whole lot cleaner if I could just deal with the button presses locally.

And things get even worse if you are creating more than one alert in the same class. Then all the alerts get filtered through the same delegate method--and you will need to separate them before responding to the button presses.

For a while now, I've thought that we really need a clean block-based API for alerts. That would let me define both my alert and the responses to the button presses locally. No more need for a delegate. Everything could be nice, clean and simple.

I had in mind something that would work like this:

RWAlertManager *manager = [RWAlertManager sharedAlertManager];

[manager createAlertWithTitle:@"Some Random Error"
                      message:@"Do you want me to delete all the user's data?"];

 
[manager addButtonWithTitle:@"Yes"
                   andBlock:^{ [self deleteEverything]; }];

[manager addButtonWithTitle:@"No"
                   andBlock:^{ [self saveDataAndExit]; }];

[manager showAlert];

There are some problems with this. For example, we need to call createAlertWithTitle:message: first. Then we need to call all our addButtonWithTitle:andBlock: calls. Finally we need to call showAlert. Bad things will probably happen if we get these out of order. And we'd probably need some rather messy code behind the scenes to track everything.

Still, to my mind, that would be a huge improvement over the current UIAlertView API.

However, as many of you know, I've been experimenting with the new RubyMotion SDK recently. So far, I'm very impressed with it. Yes, there are a few things that I'd like to see implemented before I try using it for a real-world project. Fortunately, HipByte seems to be developing at a breakneck speed, so I'm sure those gaps will be filled in soon.

This also means I've had Ruby on the brain lately. One of the things I dearly love about Ruby is the ease with which I can pull off really crafty metaprogramming feats (basically writing code that writes code), or create internal Domain Specific Languages (DSLs).

So, this got me to thinking. Both Ruby and Objective-C are highly-dynamic, object-oriented, message passing languages. They are both heavily influenced by Smalltalk. Why can't I use the same metaprogramming tricks I'd use in Ruby to create an even slicker interface here. Maybe not something you'd call a full-fledged DSL, but at least a fluent API.

Admittedly, Objective-C is a much more ceremonial language (requiring a lot more typing and boilerplate than Ruby). Also, its syntax isn't nearly as flexible as Ruby. And the block syntax, frankly, sucks. So, I can probably only push this so far. Still, I thought it was worth a try.

The Solution

I decided to create an API, that would let me show an alert with the following code:

[[RWAlertManager sharedAlertManager]
 showAlertWithTitle:@"Some Random Error"
 message:@"Do you want to delete all the user's data?"
 configurationBlock:^(AddButtonBlock addButton) {
                 
     addButton(@"YES", ^{ [self deleteEverything]; });
     addButton(@"NO",  ^{ [self saveDataAndExit]; });
}];

OK, hold on to your hatters. We're about to go down the rabbit hole.

Here's the basics. showAlertWithTitle:message:configurationBlock: will create an alert view, then pass a block back which I can use to configure the view. Once I'm done, it will automatically display the view.

Inside the configuration block, the system hands me an AddButtonBlock. We can ignore the fact that this is a block--instead think of it as a function that we call to add buttons to our view. However, this function is only accessible inside our configuration block.

Note: This is a great example of encapsulation. Unlike my first proposed solution, we cannot call addButton at the wrong time. It is only accessible inside my configuration block. More importantly, this type of encapsulation falls outside the normal object/instance variable/method encapsulation we're accustomed to in Objective-C--though it's the type of thing I do all the time in Ruby or Lisp.

The addButton block (function) takes two arguments, a string (the button's title) and a block (the action that will occur when that button is pressed). If we don't want any actions to occur beyond simply dismissing the alert, we can pass in nil.

So, basically we have a block inside a block inside a block. Confused yet? Don't worry. It's not that bad.

The Implementation

RWAlertManager is a singleton. It will use two dictionaries. One will store the alert views (just in case we have more than one alert active at a time). The other will be a dictionary containing arrays of blocks--one array for each alert view, with one block for each button.

We'll auto-generate locally-unique keys for our views, and use them to access both dictionaries. The code for all of this is fairly straightforward, so I won't go into it here. You can check it out in the source code.

The real work starts with showAlertWithTitle:message:configurationBlock:.

- (void)showAlertWithTitle:(NSString *)title 
                   message:(NSString *)message 
        configurationBlock:(void (^)(AddButtonBlock addButton))config {
    
    // Setup
    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title
                                                    message:message
                                                   delegate:self
                                          cancelButtonTitle:nil
                                          otherButtonTitles:nil];
    
    RWAlertID *key = [self keyForAlertView:alert];
    NSMutableArray *blockList = [NSMutableArray array];
    
    // Build the config blocks
    AddButtonBlock addButton = [self createAddButtonBlockForAlertView:alert
                                                         andBlockList:blockList];
    
    // Call the config block
    if (config != nil) config(addButton);
    
    // Save Data
    [self.alerts setObject:alert forKey:key];
    [self.blockLists setObject:blockList forKey:key];
    
    // Display the view
    [alert show];
    
    
    // if it doesn't have any buttons, autodismiss it
    if ([blockList count] == 0) {
        
        [self autoDismissAlertView:alert];
    }
    
}

I start by creating a UIAlertView with no buttons, setting self as the delegate. Then I generate my key, and create a mutable array to hold my list of blocks.

Next, I call createAddButtonBlockForAlertView:andBlockList: to create my add button block--we'll cover that in a second.

Then, if I have a configuration block, I call it, passing the addButton block.

Finally, I save the alert and the blacklist, and display the alert. If my alert doesn't have any buttons, I call autoDismissAlertView: to automatically dismiss the alert after a few seconds.

Buttonless alerts aren't very pretty--there's a bit too much space at the bottom for my tastes. Still, sine you can use the API to create a buttonless alert, we'd better provide reasonable behaviors for handling them.

AddButtonBlock is defined in the header file as:

typedef void (^ResponseBlock) (void);
typedef void (^AddButtonBlock) (NSString *title, ResponseBlock response);

Typdeffing blocks is one of the ways to make them a little less onerous in Objective-C.

OK, nothing too shocking there. Let's look at createAddButtonBlockForAlertView:andBlockList:.

- (AddButtonBlock)createAddButtonBlockForAlertView:(UIAlertView *)alert 
                                      andBlockList:(NSMutableArray *)blockList {
    
    return ^(NSString *title, ResponseBlock block){
        
        [alert addButtonWithTitle:title];
        
        if (block != nil) {
            
            [blockList addObject:block];
            
        } else {
            
            [blockList addObject:^{}];
            
        }
    };
}

First off, I know. I screwed up the naming. We're not supposed to use "and" when introducing additional parameters in a method. I'll fix that when I refactor the code.

Moving on...I create a block that consumes a string and a ResponseBlock. We've already seen the typedef for ResponseBlock. It's basically a block that doesn't take any arguments, and which doesn't return any values.

The block I'm creating will capture the alert and blockList variables, letting me access them inside my newly created block. Since these are objects, my block will retain them (if necessary), and I can actually call their methods (thus modify them).

When this new block is run, it will add a new button to the alert, using the provided title. Then it will add the provided ResponseBlock to the block list. If you pass nil for the response block, it will simply create a null-op block and add that instead. That way there's always a valid block for each button (even if the block doesn't do anything).

This is one of the few places where Objective-C's "you can safely call any methods on nil" attitude can get you in trouble. Calling a null-valued block will crash your app.

Now we need to respond to button presses. Let's look at the UIAlertViewDelegate method alertView:willDismissWithButtonIndex:.

- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {
    
    RWAlertID *key = [self keyForAlertView:alertView];
    [self processButtonPress:buttonIndex forAlertKey:key];

    // Now remove the alert and it's blocks -- deleting them from memory
    [self.alerts removeObjectForKey:key];
    [self.blockLists removeObjectForKey:key];
    
}

Here we simply get the key for the alert view. Then we call processButtonPress:forAlertKey: to handle dispatching to the correct block. We'll look at that code next. Finally we remove the alert and block list from their respective dictionaries. This means we will no longer hold any references to them, and they will be deallocated from memory. If you haven't guessed yet, I'm using ARC here. However, I think the Manually Managed Memory version would be nearly identical (with some additional complexity for moving blocks into the heap).

processButtonPress:forAlertKey: is almost as simple:

-(void)processButtonPress:(NSInteger)buttonIndex forAlertKey:(RWAlertID *)key {
    
    // Ignore negative indicies
    if (buttonIndex < 0) return;
    
    NSArray *blocks = [self.blockLists objectForKey:key];
    ResponseBlock block = [blocks objectAtIndex:buttonIndex];
    
    block();
}

If the button index is less than 0 we simply return. We'll use this to dismiss buttonless alerts. Next, we use our key to get the array of blocks from the block dictionary. Then we use the button index to get the correct block. Finally, we fire off the block.

Last, and probably least, we have autoDismissAlertView:

- (void)autoDismissAlertView:(UIAlertView *)alert
{
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = 
    dispatch_time(DISPATCH_TIME_NOW, 
                  (long long)(delayInSeconds * NSEC_PER_SEC));
    
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        
        [alert dismissWithClickedButtonIndex:-1 animated:YES];
        
    });
}

Here, we simply use GDC to dispatch a block two seconds in the future. Inside this block, we call dismissWithClickedButtonIndex:animated: and pass -1 as the button index.

And that's essentially it. Yes, we end up three-layers deep in blocks--but it's not really that complicated when you examine all the pieces. Each step is, in itself, simple enough.

Let me know what you think, or if you have any questions or comments. You can grab the complete source code, including a sample harness that triggers different types of alerts here.

-Rich-