Tuesday, October 19, 2010

UIAlertView with blocks

Note: This blog is deprecated. @synthesize zach has moved to a new home, at zpasternack.org. This blog entry can be found on the new blog here. Please update your links.

When extending behavior in Objective-C (unlike in other OO languages), subclassing is not always your first choice. In particular, the Cocoa framework makes extensive use of the Delegation pattern. While this works very well in general, it can at times become cumbersome. Consider a ViewController that wants to display an alert:

//  MyViewController.m

#import “MyViewController.h”

enum {
    ButtonIndexCancel = 0,
    ButtonIndexDoItNow,
    ButtonIndexDoItLater,
    LengthyOperationAlertTag = 100
};

@implementation MyViewController

- (void) doLengthyOperationPrompt { 
    UIAlertView *anAlert = [[UIAlertView alloc] initWithTitle:@"Warning!"
                        message:@"Would you like to perform a lengthy operation?"
                       delegate:self
              cancelButtonTitle:@"Nope"
              otherButtonTitles:@"Yeah, sure", @"Meh, maybe later", nil];
    anAlert.tag = LengthyOperationAlertTag;
    [anAlert show];
    [anAlert release];
}

- (void)alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {
    if( alertView.tag == LengthyOperationTag ) {
        switch( buttonIndex ) {
            case ButtonIndexDoItNow:
                [self performLengthyOperation];
                break;
            case ButtonIndexDoItLater:
                [self scheduleLengthyOperationForLater];
            break;
        }
    }
}
@end

But Zach, I hear you say, what’s wrong with that? And I agree, it’s not too terrible taken on it’s own. Thing is, I’m betting MyViewController does a whole lot of things other than showing an alert. Meaning, the chances are good that your doLenghyOperationPrompt and alertView:willDismissWithButtonIndex: methods are actually not right next to each other. And, they’re probably not at the top either. So you have three related pieces of code (the enums, the code to show the alert, and the code to handle the button press) which are most likely physically separated from one another. Now imagine MyViewController has a half-dozen different UIAlertViews, each with different buttons and tags and code called in response. It adds up to a whole lotta ugly pretty quickly.

Wouldn’t it be better if all three related pieces of code were in the same place? In my dream, it would look like this:

//  MyViewController.m

#import “MyViewController.h”

@implementation MyViewController

- (void) doLengthyOperationPrompt { 
    enum {
        ButtonIndexCancel = 0,
        ButtonIndexDoItNow,
        ButtonIndexDoItLater
    };
 
    UIAlertView *anAlert = [[UIAlertView alloc] initWithTitle:@"Warning!"
                        message:@"Would you like to perform a lengthy operation?"
                       delegate:self
              cancelButtonTitle:@"Nope"
              otherButtonTitles:@"Yeah, sure", @"Meh, maybe later", nil];
    [anAlert showWithCompletion:^(NSInteger buttonIndex) {
        switch( buttonIndex ) {
            case ButtonIndexDoItNow:
                [self performLengthyOperation];
                break;
            case ButtonIndexDoItLater:
                [self scheduleLengthyOperationForLater];
                break;
        }
    }];
    [anAlert release];
}
@end

The code that handles the button presses is right there next to the code that displays the alert, preventing you from having to scroll around to follow the flow. In addition, because they’re in the same scope, we don’t have to declare the enum at the top - this is the only function that uses it - so it’s also there in the same place. Nice and compact, and all the related functionality is in close physical proximity. But how do we get there?

As I mentioned before, subclassing isn’t always the first choice as a means to extend functionality. My knee-jerk inclination was to implement this as a class extension to UIAlertView. Unfortunately, extensions can only add member functions, not iVars, making this approach problematic. The easy way, in this case, is to subclass.

It seems simple enough, we’ll need a showWithCompletion: member function into which we pass our block. Our class will need an iVar to hold the block. Then showWithCompletion: can set delegate to itself, and call the block on it’s own alertView:willDismissWithButtonIndex:.

It looks a little somethin’ like this:

//  ZPAlertView.h

#import 

typedef void (^AlertCompletion)(NSInteger);

@interface ZPAlertView : UIAlertView  {
    AlertCompletion completionBlock;
}
- (void) showWithCompletion:(AlertCompletion)aBlock;
@end

//  ZPAlertView.m

#import "ZPAlertView.h"

@implementation ZPAlertView

- (void) showWithCompletion:(AlertCompletion)aBlock {
    self.delegate = self;
    completionBlock = [aBlock copy];
    [self show];
}

- (void) alertView:(UIAlertView *)alertView willDismissWithButtonIndex:(NSInteger)buttonIndex {
    completionBlock(buttonIndex);
    [completionBlock release]; completionBlock = nil;
}

@end

Stupid, simple right?

We start out by creating a typedef for our block. It takes a single parameter of type NSInteger and has no return value. This isn’t quite the same as alertView:willDismissWithButtonIndex, because we don’t supply the alertView. There’s no need to, because it’s right there in the scope of our block. We add one function, showWithCompletion:, which takes the block which will be executed.

showWithCompletion: sets the delegate to self (because we’ll be handling alertView:willDismissWithButtonIndex: ourselves), stores off a copy of the completion block, and calls [self show]. We take a copy of the block, because the block was created on the stack, and will be going out of scope before we execute it later, so the copy gives a copy on the heap which will stick around until we’re done with it.

The magic happens in alertView:willDismissWithButtonIndex:. We simply call the block, and then release it. That’s it.

Using this class, we can do exactly like I wanted above, by only replacing UIAlertView with ZPAlertView:

//  MyViewController.m

#import “ZPAlertView.h”
#import “MyViewController.h”

@implementation MyViewController

- (void) doLengthyOperationPrompt { 
    enum {
        ButtonIndexCancel = 0,
        ButtonIndexDoItNow,
        ButtonIndexDoItLater
    };
 
    ZPAlertView *anAlert = [[ZPAlertView alloc] initWithTitle:@"Warning!"
                        message:@"Would you like to perform a lengthy operation?"
                       delegate:self
              cancelButtonTitle:@"Nope"
              otherButtonTitles:@"Yeah, sure", @"Meh, maybe later", nil];
    [anAlert showWithCompletion:^(NSInteger buttonIndex) {
        switch( buttonIndex ) {
            case ButtonIndexDoItNow:
                [self performLengthyOperation];
                break;
            case ButtonIndexDoItLater:
                [self scheduleLengthyOperationForLater];
                break;
        }
    }];
    [anAlert release];
}
@end


You can do lots of cool stuff with this. Want to put a text field on there? No problem!

- (void) doAlertWithTextField {
    ZPAlertView *alert = [[ZPAlertView alloc] initWithTitle:@"Hello!" 
                        message:@"Please enter your name:\n\n\n"
                       delegate:nil 
              cancelButtonTitle:nil
              otherButtonTitles:@"OK", nil];
    UITextField *nameEntryField = [[UITextField alloc] initWithFrame:CGRectMake(12, 90, 260, 25)];
    nameEntryField.backgroundColor = [UIColor whiteColor];
    nameEntryField.keyboardType = UIKeyboardTypeAlphabet;
    nameEntryField.keyboardAppearance = UIKeyboardAppearanceAlert;
    nameEntryField.autocorrectionType = UITextAutocorrectionTypeNo;
    nameEntryField.clearButtonMode = UITextFieldViewModeWhileEditing;
    [alert addSubview:nameEntryField];
    [nameEntryField becomeFirstResponder];
    [nameEntryField release];
    [alert showWithCompletion:^(NSInteger buttonIndex) {
        UIAlertView *anAlert = [[UIAlertView alloc] initWithTitle:@"Greetings!"
                    message:[NSString stringWithFormat:@"Hello, %@", nameEntryField.text]
                   delegate:nil
          cancelButtonTitle:nil
          otherButtonTitles:@"OK", nil];
        [anAlert show];
        [anAlert release];
    }];
    [alert release];
}

Normally when you do such a thing (as I’ve written about before), you need to set a tag on the UITextField so you can find it later in alertView:willDismissWithButtonIndex to get at the text. This way, there’s no need for that, because it’s still in the scope of our block.

One limitation of this is that UIAlertViewDelegate has a bunch of delegate methods aside from alertView:willDismissWithButtonIndex:. If you wanted to do something in other delegate methods, you’d need to modify it to do that. For my purposes, all I needed was willDismissWithButtonIndex. Also, showWithCompletion: is replacing whatever delegate you specified in your initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles: call, so you can’t really use any of the other delegate methods (of course, you could just call show if you wanted to do that, or just use a regular UIAlertView).

One could probably implement this as a class extension, but there are issues with doing so. For one thing, you can’t add any iVars to a class exetension, only member functions. You could get around this by, say, storing a static dictionary mapping UIAlertViews to AlertCompletion blocks. But that is kinda icky. Plus, the semantics of an NSDictionary are to copy the key, so you couldn’t use the UIAlertView as the key, unless you take the integer value of the pointer and wrap that in a NSNumber. Double icky. That’s about as far along as I’ve gotten in the process of making this into a class extension, but if someone has a better idea, I’d love to hear it.

Note that I haven’t used this in any production code yet -- PuzzleTiles 1.1 is close enough to release that I’m disinclined to make such seemingly gratuitous changes -- so I don’t know if there are any gotchas I haven’t thought of. Use it at your own risk, I’m saying. If you do find use for it, or have ideas for how to improve it, let me know!

Thursday, October 14, 2010

Stupid UIAlertView Tricks Part Deux

Note: This blog is deprecated. @synthesize zach has moved to a new home, at zpasternack.org. This blog entry can be found on the new blog here. Please update your links.

Another Stackoverflow question has prompted me to yet again revisit the issue of putting random views into a UIAlertView.

If you missed my previous installments, check out UIAlertView with UITextField (which is actually moderately useful at times), and UIAlertView with UIWebView, which is pretty much not good for anything except guffaws.

In this installment, we'll jam a UITableView in there, and even throw in some UISwitch controls for good measure.

Note: never do this.  Though I don't believe it's a direct violation of the Almighty Apple Book Of Holiness Human Interface Guidelines, it would surely make most iPhone UI Geeks cringe.  I'm not a UI hardliner, but I wouldn't do this in one of my apps.  Neither Fat Apps, nor myself are responsible for any App Store rejections, loss of life, loss of limb, or loss of credibility that may result from using this code in an actual app.  This is for fun.

Now then.  Much like putting any other random view in an alert view, there's really not that much work to do beyond what you'd normally do if you were making such a view without the alert view.  In the case of a UITableView, you need to have a class which implements UITableViewDataSource.  Create the cells in tableView:cellForRowAtIndexPath:, and add your controls as the accessoryView, like so:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString* const SwitchCellID = @"SwitchCell";
UITableViewCell* aCell = [tableView dequeueReusableCellWithIdentifier:SwitchCellID];
if( aCell == nil ) {
aCell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:SwitchCellID] autorelease];
aCell.textLabel.text = [NSString stringWithFormat:@"Option %d", [indexPath row] + 1];
aCell.selectionStyle = UITableViewCellSelectionStyleNone;
UISwitch *switchView = [[UISwitch alloc] initWithFrame:CGRectZero];
aCell.accessoryView = switchView;
[switchView setOn:YES animated:NO];
[switchView addTarget:self action:@selector(soundSwitched:) forControlEvents:UIControlEventValueChanged];
[switchView release];
}

return aCell;
}

There's no magic here: it's what I do in the PuzzleTiles options screen, and it's what Apple does in the Settings app.  No big whoop.

Alright, now let's jam this bad boy into an alert view.

- (void) doAlertWithTableView {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Preferences"
message:@"\n\n\n\n\n\n\n"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"OK", nil];

UITableView *myView = [[[UITableView alloc] initWithFrame:CGRectMake(10, 40, 264, 150)
style:UITableViewStyleGrouped] autorelease];
myView.delegate = self;
myView.dataSource = self;
myView.backgroundColor = [UIColor clearColor];
[alert addSubview:myView];

[alert show];
[alert release];
}

That's it!  The only real trick (just like with doing with with a UITextField or a UIWebView, is to put some linefeeds in your message to make the alert taller to fit your view.

Here's how she looks:

UIAlertView with UITableView screenshot

Next time I'll try writing a post that *doesn't* feature a UIAlertView! Yay!

Friday, September 24, 2010

Stupid UIAlertView Tricks: Part I

Note: This blog is deprecated. @synthesize zach has moved to a new home, at zpasternack.org. This blog entry can be found on the new blog here. Please update your links.

Earlier this evening I was answering a StackOverflow question about putting a UITextView onto a UIAlertView.  I had done this before with UITextFields (I wrote a post about it), but I'd never tried it before with a UITextView.  Well, it turns out to totally work (see my answer here, if you care).

However, it got me thinking about jamming random other views into a UIAlertView.  The logical conclusion of which was this:  StackOverflow in a UIAlertView.


Why?  I dunno.  Cool though, right?

- (void) doAlertViewWithWebView {
 UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"stackoverflow" 
             message:@"\n\n\n\n\n\n\n\n\n\n\n\n"
               delegate:nil 
            cancelButtonTitle:nil
            otherButtonTitles:@"OK", nil];
 
 UIWebView *myView = [[[UIWebView alloc] initWithFrame:CGRectMake(10, 40, 264, 254)] autorelease];
 myView.scalesPageToFit = YES;
 [alert addSubview:myView];
 
 [myView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.stackoverflow.com"]]];
 
 [alert show];
 [alert release]; 
}

Thursday, September 23, 2010

UIAlertView with UITextField

Note: This blog is deprecated. @synthesize zach has moved to a new home, at zpasternack.org. This blog entry can be found on the new blog here. Please update your links.

It's sometimes useful to display an alert with a text box for simple data entry.  It's simple, effective, and is a pretty reasonable UI, in my opinion.  For example, PuzzleTiles does this as kind of a ghetto name entry prompt for adding your name to the high score list.  

Fortunately, Apple provides an API for adding text fields to alert views.  Unfortunately, they're private.  It used to be you could sneak that by the app review team, but those days are over.  Your app will be rejected if you use these calls, take it from me.

Good news, though: manually adding your own text field to an alert view is easy, and fun.  Dig it:

#define kTag_EnterNameAlert  1
#define kTag_NameEmtryField  100

- (void) doAlertWithTextField {
 UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Congratulations!" 
             message:@"You earned a top score! Enter your name:\n\n"
               delegate:self 
            cancelButtonTitle:nil
            otherButtonTitles:@"OK", nil];
 alert.tag = kTag_EnterNameAlert;
 
 CGRect entryFieldRect = CGRectZero;
 if( UIDeviceOrientationIsPortrait( [UIApplication sharedApplication].statusBarOrientation ) ) {
  entryFieldRect = CGRectMake(12, 90, 260, 25);
 }
 else {
  entryFieldRect = CGRectMake(12, 72, 260, 25);
 }
 
 UITextField *nameEntryField = [[UITextField alloc] initWithFrame:entryFieldRect];
 nameEntryField.tag = kTag_NameEmtryField;
 nameEntryField.backgroundColor = [UIColor whiteColor];
 nameEntryField.keyboardType = UIKeyboardTypeAlphabet;
 nameEntryField.keyboardAppearance = UIKeyboardAppearanceAlert;
 nameEntryField.autocorrectionType = UITextAutocorrectionTypeNo;
 nameEntryField.clearButtonMode = UITextFieldViewModeWhileEditing;
 [alert addSubview:nameEntryField];
 [nameEntryField becomeFirstResponder];
 [nameEntryField release];
 
 [alert show];
 [alert release];
}

You more-or-less make a UITextField and add it to the UIAlertView via addSubview:. There are some things to note, in no particular order:

  • Put some extra linefeeds at the end of your message.  This will make the alert view taller, which makes room for the text field.  You'll need to play around with it to get it looking just right.
  • You're going to need to hardcode the coordinates of the text field, and depending on the message, the location might need to change.  Again, play with it.  Don't half-ass it; iPhone apps need to be pretty.  Note that if you support landscape mode, the layout will be ever-so-slightly different.
  • Set the text field's keyboardAppearance to UIKeyboardAppearanceAlert; otherwise the keyboard will overlap with the alert.
  • If you're expecting the user to enter a name, you probably want to turn off autocorrection by setting the text field's autocorrectionType to UITextAutocorrectionTypeNo
  • If you intend to do something with the text later, set the text field's tag so you can easily retrieve it later (like, in alertView:willDismissWithButtonIndex;.
The result looks like this: