Sunday, March 24, 2013

Displaying an alert view with blocks

The default method for displaying an alert view is somewhat cumbersome if you have a dialog with many possible choices. I had a table cell that had two buttons, one that would add to a number, and one that would subtract. I wanted to then add the ability to directly set the number. As I started adding more choices, I ended up with a cumbersome dialog. The default alert view protocol expects only one callback, so you have to track what dialog you are showing, and have an case statement or if/else block for that. Anytime you see that construct, you know it is time to refactor.

Before

This code starts to get ugly quickly, multiple repeated lines, and the need to put an if/else in my alert callback.  The logic was also hard to follow.

- (IBAction)subtract:(id)sender {
    UIAlertView * alert = [[UIAlertView alloc] initWithTitle:self.tracker.subtractString message:@"How much:" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:self.tracker.subtractString,nil];
    alert.alertViewStyle = UIAlertViewStylePlainTextInput;
    UITextField * alertTextField = [alert textFieldAtIndex:0];
    alertTextField.keyboardType = UIKeyboardTypeNumberPad;
    alertTextField.placeholder = [NSString stringWithFormat:@"How much to %@",self.tracker.subtractString];
    self.add=false;
    [alert show];
}
- (IBAction)add:(id)sender {
    UIAlertView * alert = [[UIAlertView alloc] initWithTitle:self.tracker.addString message:@"How much:" delegate:self cancelButtonTitle:@"Continue" otherButtonTitles:self.tracker.addString,nil];
        alert.alertViewStyle = UIAlertViewStylePlainTextInput;
    UITextField * alertTextField = [alert textFieldAtIndex:0];
    alertTextField.keyboardType = UIKeyboardTypeNumberPad;
    alertTextField.placeholder = [NSString stringWithFormat:@"How much to %@",self.tracker.addString];
    @"Enter how much to heal";
    self.add=true;
     [alert show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    if (alertView.cancelButtonIndex!=buttonIndex)
    {
        int num= [[[alertView textFieldAtIndex:0] text] intValue];
        if (self.add)
        {
            self.tracker.currentValue+=num;

        }
        else
        {
            self.tracker.currentValue-=num;
        }
        NSError *error;
        [self.tracker.managedObjectContext save:&error];
        //self.currentLabel.text=[NSString stringWithFormat:@"%d",self.tracker.currentValue];
    }
}

The ability to create a block, an inline method, allows this code to be extracted into a common class, and then made FAR simpler for the receiver.

The first step was creating the alert class that can display an alert view. This class assumes that there is only one prompt active at a time.
@interface AlertButtonHelper : NSObject


@property(nonatomic, copy) void (^action)(NSString *);

- (void)promptForInput:(NSString *)prompt title:(NSString *)title withAcceptButton:(NSString *)acceptButton
          endingAction:(void (^)(NSString *))action keyboardType:(enum UIKeyboardType)keyboardType;

@end

@implementation AlertButtonHelper {


}
// The main command to prompt.
- (void)promptForInput:(NSString *)prompt title:(NSString *)title withAcceptButton:(NSString *)acceptButton endingAction:(void (^)(NSString *))action keyboardType:(enum UIKeyboardType)keyboardType {
        UIAlertView * alert = [[UIAlertView alloc] initWithTitle:title message:prompt delegate:self cancelButtonTitle:@"Cancel" 
                                           otherButtonTitles:acceptButton,nil];
        alert.alertViewStyle = UIAlertViewStylePlainTextInput;
        UITextField * alertTextField = [alert textFieldAtIndex:0];
        alertTextField.keyboardType = keyboardType;
        alertTextField.placeholder = [NSString stringWithFormat:prompt];
        
         [alert show];
        self.action=action;
}

// The standard alert callbakc.
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{

    NSString *value = [alertView textFieldAtIndex:0].text;
    if (self.action!=nil)
    {
        self.action(value);
    }
    
}
@end

I ended up with a bunch of blocks that looked like this. Where the only difference was the one line where the number was used.
            void (^actionToTake)(NSString *) = ^(NSString *string) {
                int num= [string intValue];
                self.tracker.currentValue=num;
                NSError *error;
                [self.tracker.managedObjectContext save:&error];

            };
    
I'm using AppCode, which has a nifty extract as block feature.  I used it to transform it into
- (void (^)(NSString *))getModifyFunction:(void (^)(int))modifyValue {
    void (^actionToTake)(NSString *) = ^(NSString *string) {
            int num= [string intValue];
        modifyValue(num);
        NSError *error;
            [self.tracker.managedObjectContext save:&error];

        };
    return actionToTake;
}


 void (^actionToTake)(NSString *)= [self getModifyFunction:^(int num) {
        self.tracker.maxValue = num;
    }];

The final client code is:

- (IBAction)subtract:(id)sender {

    NSString *str=[NSString stringWithFormat:@"How much to %@",self.tracker.subtractString]  ;

    void (^actionToTake)(NSString *)= [self getModifyFunction:^(int num) {
        self.tracker.currentValue -= num;
    }];
        [self.helper promptForInput:str title:self.tracker.subtractString withAcceptButton:self.tracker.subtractString
                       endingAction:actionToTake keyboardType:UIKeyboardTypeNumberPad];

}


- (IBAction)maxSelected:(id)sender {
    NSString *str=[NSString stringWithFormat:@"Enter max Value"]  ;

    void (^actionToTake)(NSString *)= [self getModifyFunction:^(int num) {
        self.tracker.maxValue = num;
    }];
    [self.helper promptForInput:str title:@"Max Value" withAcceptButton:@"Set Max"
                       endingAction:actionToTake keyboardType:UIKeyboardTypeNumberPad];


}

- (void (^)(NSString *))getModifyFunction:(void (^)(int))modifyValue {
    void (^actionToTake)(NSString *) = ^(NSString *string) {
            int num= [string intValue];
        modifyValue(num);
        NSError *error;
            [self.tracker.managedObjectContext save:&error];

        };
    return actionToTake;
}

- (IBAction)add:(id)sender {
    NSString *str=[NSString stringWithFormat:@"How much to %@",self.tracker.addString]  ;

    void (^actionToTake)(NSString *)= [self getModifyFunction:^(int num) {
        self.tracker.maxValue += num;
    }];

    [self.helper promptForInput:str title:self.tracker.addString withAcceptButton:self.tracker.addString
                   endingAction:actionToTake keyboardType:UIKeyboardTypeNumberPad];
}

- (IBAction)currentSelected:(id)sender {
    NSString *str=[NSString stringWithFormat:@"Enter current Value"]  ;

    void (^actionToTake)(NSString *)= [self getModifyFunction:^(int num) {
        self.tracker.currentValue = num;
    }];
            [self.helper promptForInput:str title:@"Set Value" withAcceptButton:@"Set Value"
                           endingAction:actionToTake keyboardType:UIKeyboardTypeNumberPad];
}


I initialize helper in the init method.

 if (self.helper==nil)
    {
        self.helper=[[AlertButtonHelper  alloc] init];
    }
The result has strange syntax, but eliminates a lot of redundant code. The use of blocks is a great way to isolate the 'different' items. The next stage would be to extract a protocol for the alert interface, so that the method of displaying alerts could change if desired.