March 09, 2015

UIAlertController For iOS 7 Or When One Might Use Associated References

Today I want to share with you one of my pieces of code that has proved to be quite useful. Sometimes what you need is not something brilliant or especially sophisticated, but something steady and dependable, doing all that annoying work for you. Well that’s DAAlertController.

I did not think that I would be ever so happy to learn that a certain cocoa API got deprecated. But let's hand it to UIAlertView and UIActionSheet - they were always different and not "good different". I could not have said it better than Matt:

“From its very inception, UIAlertView has been laden with vulgar concessions, sacrificing formality and correctness for the whims of an eager developer audience”.

Fortunately the new UIAlertController is everything that UIAlertView was not. First of all, a subclass of UIViewController, then it has these beautiful long objective-c style methods allowing the user to specify actions (buttons) at any time (constructor dependency injections were never meant for methods with variable length parameters, right?), and finally completion handlers always win over delegates these days.

And even though it preserved some of UIAlertView’s ”blackbox”-ness, UIAlertController does a much better job handling edge cases. Have you ever tried to have 10 buttons in UIAlertView? Yes, I know, alert is not the best “choice" for that multi-choice scenario, but if the API allows you to specify that many buttons, it should either handle them nicely or fail gracefully. Well, unlike it’s predecessor, UIAlertController will put buttons on a scroll view and will not sacrifice the title.

So if you are a luckier developer and only target iOS 8+ devices, enjoy the new API, otherwise check DAAlertController out, it’s a real timesaver.

When you have to support both iOS 8 and 7 and use alert views or actions sheets in your projects, you usually end up with a large if / else statement. Code inside if (NSStringFromClass([UIAlertController class])) {...} is usually quite neat due to the block based API, while else {...} can turn into a real nightmare and often will make your class implement a UIAlertViewDelegate and/or UIActionSheetDelegate protocols. So when you finally decide to drop iOS7 support, getting rid of old alert views will not be that easy.
If only there was a way to use completion handlers for old UIAlertViews and UIActionSheets… Well that’s exactly what DAAlertController does - it incapsulates these large if/else statements and has a nice UIAlertController-like API for both iOS 8 and 7.

Let’s take a look at the case of a simple alert view:

DAAlertAction *cancelAction = [DAAlertAction actionWithTitle:@"Cancel"
    style:DAAlertActionStyleCancel handler:nil];

DAAlertAction *signOutAction = [DAAlertAction actionWithTitle:@"Sign out"
    style:DAAlertActionStyleDestructive handler:^{
        // perform sign out
    }];

[DAAlertController showAlertViewInViewController:self
    withTitle:@"Are you sure you want to sign out?"
    message:@"If you sign out of your account all photos will be removed from this iphone."
    actions:@[cancelAction, signOutAction]];

Here is what you will get for iOS 8 and 7: (UIAlertView does not support destructive buttons so "Sign out" button will be rendered as a default button)

If UIAlertContoller is available DAAlertController will just pass all the work to it, otherwise it will use associated references (explained in detail later) to invoke action handlers when buttons are clicked just like UIAlertController would.

Naturally, DAAlertController is limited to what the old UIAlertView and UIActionSheet could do: - action sheets can only have one destructive button (buttons for other destructive actions will be rendered as default buttons) - alert views cannot have any destructive buttons (again, all buttons will look like default buttons) - alert views can only have up to 2 text fields - action sheets can only have a title; a message will not be displayed - you can only specify up to 10 actions for iOS 7 (otherwise alert views / action sheets would not be rendered properly anyways)

There is also one bonus: one thing that DAAlertController does better than UIAlertController is autoenabling buttons based on the contents of textfield(s). In the old API there was just one convenient delegate method alertViewShouldEnableFirstOtherButton; now with UIAlertController it’s a whole big deal - you need to subscribe to UITextFieldTextDidChangeNotification in text field configuration blocks and remember to unsubscribe when necessary, which is possible in any of the action's handlers, so it really gets messy. So here is what you would do with DAAlertController in the following use case: imagine you want to present a sign up dialog, “full name” is optional, but “nickname” is required and should be at least 5 characters long:

DAAlertAction *cancelAction = [DAAlertAction actionWithTitle:@"Cancel"
    style:DAAlertActionStyleCancel
    handler:nil];

DAAlertAction *signUpAction = [DAAlertAction actionWithTitle:@"Sign up"
    style:DAAlertActionStyleDefault handler:^{
        // perform sign up
    }];

[DAAlertController showAlertViewInViewController:self
    withTitle:@"Sign up"
    message:@"Please choose a nick name."
    actions:@[cancelAction, signUpAction]
    numberOfTextFields:2
    textFieldsConfigurationHandler:^(NSArray *textFields)
    {
        UITextField *nickNameTextField = [textFields firstObject];
        nickNameTextField.placeholder = @"Nick name";
        UITextField *fullNameTextField = [textFields lastObject];
        fullNameTextField.placeholder = @"Full name";
    } validationBlock:^BOOL(NSArray *textFields) {
        UITextField *nickNameTextField = [textFields firstObject];
        return nickNameTextField.text.length >= 5;
    }];

This little snippet will do it for both iOS 7 and 8 and it’s 6 times less code than you’d have using UIAlertController API.

Methods for presenting action sheets are quite similar, but they also include parameters for sourceView or barButttonItem and permittedArrowDirections.

So that’s it for you guys looking for a simple Util to make UIAlertController work for iOS 7 (in a way), go grab a pod or fork a github repo. For those of you interested in my struggle with UIAlertView and how associated references were a help, here is my story:

As you probably noticed, DAAlertController takes an array of DAAlertActions as a parameter; the challenge is to transform that array into a “nil terminated” otherButtonTitles to pass into a UIAlertView designated initialiser initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles: You might wonder why don’t just pass nilas otherButtonTitles and configure them later (using addButtonWithTitle: method). This is exactly what I did at first and was the reason for the first bug report to apple. It turns out that if you do pass nil as otherButtonTitles and regardless of whether you specify them later, firstOtherButtonIndex will always remain -1. And this results in alertViewShouldEnableFirstOtherButton never being called, which is something we cannot afford if we want to disable buttons when there is no text in alert view’s textfiled(s). This gives us no choice other than to specify other button titles in the initialiser method.
So back to the problem of turning an NSArray into a variable argument list. Well, if you know of a way to do that in an ARC environment, please get in touch with me, and I will buy you a beer. ("Cocoa is my girlfriend” has a solution for a non-ARC case). Anyways, variadic methods is something I no longer want to get involved with. Who knows how they are doing the “shifting” (variable argument lists have no introspection), especially at a time when extra 32bits can always hide around the corner. So I decided to fight variable argument lists with their own essence - being nil terminated:

NSArray *otherButtonTitles = @[...];

...otherButtonTitles:(otherButtonTitles.count > 0) ? otherButtonTitles[0] : nil,
(otherButtonTitles.count > 1) ? otherButtonTitles[1] : nil,
(otherButtonTitles.count > 2) ? otherButtonTitles[2] : nil,
(otherButtonTitles.count > 3) ? otherButtonTitles[3] : nil, 
... ,nil];

Not the prettiest code I’ve ever written, but it does the trick and will work for any architecture.
I only check for 10 buttons, because if you need more, you should really consider using a custom popover instead of UIAlertView.

So we managed to specify titles for buttons; now we want to trigger action handlers when they are “clicked.” The UIAlertViewDelegate only notifies us about the index of a button clicked. Here usage of associated references seemed totally justified for me (for once!) so I went for it. First you want to include, then do the actual “association” :

objc_setAssociatedObject(alertView, @“otherActions", otherActions, OBJC_ASSOCIATION_COPY);

and later, when the button is clicked, you’ll be able to access otherActions of the given alertView

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if (buttonIndex != alertView.cancelButtonIndex) {
        NSArray *otherActions = objc_getAssociatedObject(alertView, @"otherActions");
        DAAlertAction *selectedAction = otherActions[buttonIndex - alertView.firstOtherButtonIndex];
        if (selectedAction.handler) {
            selectedAction.handler();
        }
    }

    // ...
}

One more thing to notice is that objects being “associated” need to implement <NSCopying> protocol.

There are more weird UIAlertController/UIActionSheet issues (like tuning UIAlertViewStyleLoginAndPasswordInputtextfield to serve our needs, fighting with UIActionSheet not to show cancel button on iPad in some cases, etc) dealt with in DAAlertController.m, you can find more details in comments there. I just hope that soon cocoa will not leave us any excuse to use associated references whatsoever.

Please enjoy and fork!

We would love to hear from you.

This form sends a email right into our inbox. One of us will get back to you as soon as possible.

Thanks for contacting us!

We will respond as soon as possible - normally, within a day.

Want to get in touch even faster? Then, please, try:

Calling us at +1-424-777-2422.

Skyping factorial.complexity.

Something went wrong :(

We are very sorry. Here are some alternative ways to contact us:

Want to get in touch even faster? Then, please, try:

Phone +1-424-777-2422

Skype factorial.complexity

Email info@f17y.com