October 31, 2009

A Truly Modal UIAlertView

I don't know why it took me so long to realize this, but UIAlertViews are not modal. Yes, they are modal as far as the UI is concerned, but your code continues running after you call the show method.
I discovered this while making a callback in my TableFormEditor package. In the callback, the form is asking the delegate "is it ok to save this data?". My delegate takes a quick look, sees something wrong, and fires up a UIAlertView to get the users input if it is ok or not. However, the [alert show] call came back immediately and the form was given a bogus answer, while the dialog was still waiting for user input. Dang.
After a bit of research, it seems that Mac UI frameworks do not embrace the concept of pure modal dialogs. I've read both sides of the argument, but it really shouldn't be an argument at all. You *should* design your code to not rely on modal behavior and instead make use of the delegate concept, but you *should* also have the ability to do modal if your code warrants it.
Sure, I could rewrite my code to move the logic from the location it belongs over to the delegate, but this creates an unmaintainable mess in my code. This is one case where a modal would really keep things neat and tidy. To address this in a general way, I created a wrapper class around UIAlertView that shows a modal dialog. The class is called JESAlert. The show method will not return until the user selects a button, and it will return the selected button index.
- (int) show;
To simplify things, this class does not allow a delegate, so the init method is similar to the original UIAlertView init but without the delegate parameter.
- (id) initWithTitle:(NSString*) title
             message:(NSString*) message
   cancelButtonTitle:(NSString*) cancelButtonTitle
   otherButtonTitles:(NSString*) otherButtonTitles, ...;

Simply call this init, then call the show method. What it returns is the selected button index.
I also enhanced the UIAlertView interface to allow for an array of otherButtonTitles, instead of just the varargs that is currently supported. I needed this feature once upon a time, so I added it.
- (id) initWithTitle:(NSString*) title
             message:(NSString*) message
   cancelButtonTitle:(NSString*) cancelButtonTitle
otherButtonTitleArray:(NSArray*) otherButtonTitles;
And lastly, since I am on a JESON kick, I have created a JESON-aware interface. You can define your UIAlertView properties in a JESON file and deploy that as a resource with your application. This removes the typical static text settings from your code and moves it into a JESON file.
To use it this way, simply use the plain init method (or combine alloc and init with a new), and call the showWithJeson: method. The filename is expected to be a resource bundle stored in your application. This will also return the button index.
JESAlert* alert = [JESAlert new];
int button = [alert showWithFile:@"alert.jes"];
The JESON file contents look like this:
alert {
    title = "Your alert title" 
    message = "More details of your message" 
    cancelButtonTitle = "cancel button title or null"
    otherButtonTitles = ["button 1", "button 2"]
}

The JESON portion of JESAlert is a bit more feature rich than the init approach. You can also run the normal, non-modal version of UIAlertView, but build the properties in a JESON file. Just add the modal = false property. You can also specify a delegate by setting the delegate property.
For example, the old-fashioned way:
UIAlertView* alert = [[UIAlertView alloc] initWithTitle:@"An error"
  message:@"Something really bad happened"
  delegate:self
cancelButtonTitle:@"Stop doing it"
otherButtonTitles:@"try again", @"try harder", nil];

[alert show];
And here is how it could be set up in a JESON file:
alert {
    title = "An error" 
    message = "Something really bad happened" 
    cancelButtonTitle = "Stop doing it"
    otherButtonTitles = ["try again", "try harder"]
    delegate = self
    modal = false
}
And accessed in the code like this:
JESAlert* alert = [JESAlert new];
alert.jesonContext = self;
[alert showWithFile:@"jeson_alert.jes"];
The jesonContext property is needed to handle variable references in your JESON text, like the "self" used above on the delegate property.
The JESON support can be easily disabled if you are not so inclined, as it requires the JESON library available here. To disable JESON support, simply comment out the define at the top of the JESAlert.h file.
//#define USE_JESON
I have not yet taken the time to create an Xcode project for this, so for now I offer this class as a .h/.m pair of files. The code is available on the Osmorphis group: JESAlert-1.0.zip.

2 comments:

  1. Prog receives EXEC_BAD_ACCESS signal when running [[NSrunLoop mainRunLoop] RunUntilDate ...

    ReplyDelete
  2. Yes, this annoys me too. I just posted about it in fact and the first of my two solutions looks a lot like yours (minus the JESON stuff). See http://marc-abramowitz.com/archives/2010/06/09/the-quest-for-a-better-uialertview/

    ReplyDelete