Monday, May 17, 2010

Custom Alert Views

Quite some time ago, I posted a way to accept text input using an alert view. The problem with that technique is that it relies on one of Apple's private APIs. I've had many people ask me how to use that technique without getting rejected in the review process. The short answer is: you can't.

What you can do, however, is create your own class that simulates the behavior of UIAlertView. You can find a sample implementation of the technique discussed in this post by downloading this project. In this simple example, we're going to create an alert view that lets the user enter a value into a single text field. The same technique can be used to present any view that you can build in Interface Builder. From a programming perspective, you will not get rejected for using private APIs if you use this technique, but be aware that you can still get rejected for HIG violations, so make sure you're familiar with the HIG.

There are many ways to mimic the behavior of the UIAlertView. One of the most common ways I've seen is to simply design an alert view in the nib of the view controller that needs to present it. This works, but it's a bit messy and pretty much completely non-reusable.

A better approach is to design the alert view as its own view controller and nib pair. We can even model it after UIAlertView's fire-and-forget approach so that the calling code can look exactly like code to create and display a UIAlertView. Before we get started, we need a few resources.

You've probably noticed that when you use an alert view, the active view grays out a bit. I don't know for sure how Apple has accomplished this, but we can simulate the behavior using a PNG image with a circular gradient that goes from 60% opaque black to 40% opaque black:

behind_alert_view.png

We also need a background image for the alert view. Here's one I hacked out in Pixelmatoralert_background.png

For your own alerts, you might need to resize or turn this into a stretchable image. For simplicity's sake, I just made it the size I want it. We also need buttons. Again, in a real example, you might want to turn these into stretchable images, but I just kept things simple by making the image the size I wanted it:

alert_button.png

The next thing we need is some animation code. We could, of course, create the CAAnimation instances right in our class, but I'm a big fan of both code reuse and Objective-C categories, so instead of doing that, I've created a category on UIView that will handle the two animations we need. One animation "pops in" a view and will be used to show the alert view, the other fades in a view and will be used for the gray background image. The two animations happen at the same time so that when the alert has fully popped into view, the background view is grayed out.

The keyframe timings on the "pop in" animation code are not quite a 100% match for Apple's animation, so if anybody wants to tweak the keyframe values to make a closer match, I'd be happy to update the code with the "correct" values. Here is the category I created to hold the animations:

UIView-AlertAnimations.h
#import <Foundation/Foundation.h>


@interface UIView(AlertAnimations)
- (void)doPopInAnimation;
- (void)doPopInAnimationWithDelegate:(id)animationDelegate;
- (void)doFadeInAnimation;
- (void)doFadeInAnimationWithDelegate:(id)animationDelegate;
@end



UIView-AlertAnimations.m
#import "UIView-AlertAnimations.h"
#import <QuartzCore/QuartzCore.h>

#define kAnimationDuration 0.2555

@implementation UIView(AlertAnimations)
- (void)doPopInAnimation
{
[self doPopInAnimationWithDelegate:nil];
}

- (void)doPopInAnimationWithDelegate:(id)animationDelegate
{
CALayer *viewLayer = self.layer;
CAKeyframeAnimation* popInAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];

popInAnimation.duration = kAnimationDuration;
popInAnimation.values = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.6],
[NSNumber numberWithFloat:1.1],
[NSNumber numberWithFloat:.9],
[NSNumber numberWithFloat:1],
nil
]
;
popInAnimation.keyTimes = [NSArray arrayWithObjects:
[NSNumber numberWithFloat:0.0],
[NSNumber numberWithFloat:0.6],
[NSNumber numberWithFloat:0.8],
[NSNumber numberWithFloat:1.0],
nil
]
;
popInAnimation.delegate = animationDelegate;

[viewLayer addAnimation:popInAnimation forKey:@"transform.scale"];
}

- (void)doFadeInAnimation
{
[self doFadeInAnimationWithDelegate:nil];
}

- (void)doFadeInAnimationWithDelegate:(id)animationDelegate
{
CALayer *viewLayer = self.layer;
CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeInAnimation.fromValue = [NSNumber numberWithFloat:0.0];
fadeInAnimation.toValue = [NSNumber numberWithFloat:1.0];
fadeInAnimation.duration = kAnimationDuration;
fadeInAnimation.delegate = animationDelegate;
[viewLayer addAnimation:fadeInAnimation forKey:@"opacity"];
}

@end


As you can see, I've provided two versions of each animation, one that accepts an animation delegate and one that doesn't. This allows the calling code to set an animation delegate so it can be notified of things like when the animation finishes. We'll use a delegate for one of our animations, but we might as well have the option with both.

The next thing we need to do is create the view controller header, implementation, and nib files. In my sample code, I've named the class CustomAlertView. It may seem odd that I giving a view controller class a name that makes it sound like a view instead of something like CustomAlertViewController. You can feel free to name yours however you wish, but this controller class will actually mimic the behavior of UIAlertView and I wanted the name to reflect that. I debated with myself over the name for a bit, but ultimately decided that CustomAlertView felt better since the name would provide a clue as to how to use it. If it feels dirty to you to "lie" in the class name, then by all means, name yours differently.

We're going to need some action methods and some outlets so that our controller and nib file can interact. One outlet will be for the background image, another for the alert view itself, and one for the text field. The latter is needed so we can tell the text field to accept and resign first responder status. We'll use a single action method for both of the buttons on the alert, and we'll use the button's tag value to differentiate between the two buttons.

Because we're implementing fire-and-forget, we also need to define a protocol containing the methods the delegate can implement. Since the alert is designed to accept input, I've made the delegate method used to receive the input @required, but the other method (called only when cancelled) @optional. The enum here is just to make code more readable when it comes to dealing with the button tag values.

CustomAlertView.h
#import <UIKit/UIKit.h>

enum
{
CustomAlertViewButtonTagOk = 1000,
CustomAlertViewButtonTagCancel
}
;

@class CustomAlertView;

@protocol CustomAlertViewDelegate
@required
- (void) CustomAlertView:(CustomAlertView *)alert wasDismissedWithValue:(NSString *)value;

@optional
- (void) customAlertViewWasCancelled:(CustomAlertView *)alert;
@end



@interface CustomAlertView : UIViewController <UITextFieldDelegate>
{
UIView *alertView;
UIView *backgroundView;
UITextField *inputField;

id<NSObject, CustomAlertViewDelegate> delegate;
}

@property (nonatomic, retain) IBOutlet UIView *alertView;
@property (nonatomic, retain) IBOutlet UIView *backgroundView;
@property (nonatomic, retain) IBOutlet UITextField *inputField;

@property (nonatomic, assign) IBOutlet id<CustomAlertViewDelegate, NSObject> delegate;
- (IBAction)show;
- (IBAction)dismiss:(id)sender;
@end


Once the header file is completed and saved, we can skip over to Interface Builder and create our interface. I won't walk you through the process of building the interface, but I'll point out the important things. Here's what the view will looks like in Interface Builder:

Screen shot 2010-05-17 at 12.42.51 PM.png

Some important things:
  • The content view needs to be NOT opaque and its background color should be set to a white with 0% opacity. The view's alpha should be 1.0. Alpha is inherited by subviews, background color is not, so by making it transparent using background color, we'll be able to see all of the subviews. If we had set alpha to 0.0 instead, we wouldn't be able see the alert view;
  • The background view is a UIImageView that is the same size as the content view. Its autosize attributes are set so that it resizes with the content view and it is connected to the backgroundView outlet. It needs to not be opaque and its alpha needs to be set to 1.0. Even though we will be animating the alpha, we want the nib to reflect the final value;
  • All of the UI elements that make up the actual alert are all subviews of a single instance of UIView whose background color is also set to white with 0% opacity and is NOT opaque so that it is invisible. This view is used just to group the elements so they can be animated together and is what the alertView outlet will point to;
  • The alert view is not centered. In this case, we want it centered in the space remaining above the keyboard;
  • The OK button has its tag set to 1,000 in the attribute inspector. The Cancel button has its tag set to 1,001. These numbers match the values in the enum we created in the header file
  • File's Owner is the delegate of the text field. This allows the controller to be notified when the user hits the return key on the keyboard


Once the interface is created, all that's left to do is implement the controller class. Here is the implementation; I'll explain what's going on in a moment:

CustomAlertView.m
#import "CustomAlertView.h"
#import "UIView-AlertAnimations.h"
#import <QuartzCore/QuartzCore.h>

@interface CustomAlertView()
- (void)alertDidFadeOut;
@end


@implementation CustomAlertView
@synthesize alertView;
@synthesize backgroundView;
@synthesize inputField;
@synthesize delegate;
#pragma mark -
#pragma mark IBActions
- (IBAction)show
{
// Retaining self is odd, but we do it to make this "fire and forget"
[self retain];

// We need to add it to the window, which we can get from the delegate
id appDelegate = [[UIApplication sharedApplication] delegate];
UIWindow *window = [appDelegate window];
[window addSubview:self.view];

// Make sure the alert covers the whole window
self.view.frame = window.frame;
self.view.center = window.center;

// "Pop in" animation for alert
[alertView doPopInAnimationWithDelegate:self];

// "Fade in" animation for background
[backgroundView doFadeInAnimation];
}

- (IBAction)dismiss:(id)sender
{
[inputField resignFirstResponder];
[UIView beginAnimations:nil context:nil];
self.view.alpha = 0.0;
[UIView commitAnimations];

[self performSelector:@selector(alertDidFadeOut) withObject:nil afterDelay:0.5];



if (sender == self || [sender tag] == CustomAlertViewButtonTagOk)
[delegate CustomAlertView:self wasDismissedWithValue:inputField.text];
else
{
if ([delegate respondsToSelector:@selector(customAlertViewWasCancelled:)])
[delegate customAlertViewWasCancelled:self];
}

}

#pragma mark -
- (void)viewDidUnload
{
[super viewDidUnload];
self.alertView = nil;
self.backgroundView = nil;
self.inputField = nil;
}

- (void)dealloc
{
[alertView release];
[backgroundView release];
[inputField release];
[super dealloc];
}

#pragma mark -
#pragma mark Private Methods
- (void)alertDidFadeOut
{
[self.view removeFromSuperview];
[self autorelease];
}

#pragma mark -
#pragma mark CAAnimation Delegate Methods
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
[self.inputField becomeFirstResponder];
}

#pragma mark -
#pragma mark Text Field Delegate Methods
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self dismiss:self];
return YES;
}

@end



So, what're we doing here? First, we start by importing the category with our alert view animations and also QuartzCore.h which gives us access to all the datatypes used in Core Animation.

#import "CustomAlertView.h"
#import "UIView-AlertAnimations.h"
#import <QuartzCore/QuartzCore.h>

Next, we declare a class extension with a single method. By putting this method here, we can use it anywhere in our class without getting warnings from the compiler yet we do not advertise the existence of this method to the world. This is, essentially, a private method. In a dynamic language like Objective-C, there are no truly private methods, but since the method is not declared in the header file, that's our way of saying "this is ours, don't touch". This method, which you'll see in a moment, will be called after the alert has been dismissed to remove it from its superview. We don't want to remove it until after the fade-out animation has finished, which is why we've declared a separate method.

@interface CustomAlertView()
- (void)alertDidFadeOut;
@end


After we synthesize our properties, the first method we write is show. This is the method that gets called to, well… show the alert. I matched the method name used in UIAlertView and also made it an IBAction so that it can be triggered directly inside a nib file.

The weirdest part of this method is that it actually retains self. This is something you're generally not going to want to do. Since we've implemented our alert view as a view controller instead of a UIView subclass like UIAlertView, we need to cheat a little because a view controller is not retained by anything by virtue of its view being in the view hierarchy. This isn't wrong - we're going to bookend our retain with a release (well, actually, an autorelease) so no memory will leak, but it is unusual and not something you're going to want to use in very many places. When you retain self, you need to take a long hard look at your code and make sure you have a darn good reason for doing it. In this instance, we do.

After retaining, we grab a reference to the window by way of the application delegate and add our view to the window, matching its frame. Then we call the two animation methods we created earlier to fade in the image with the circular gradient and "pop" in the alert view:

- (IBAction)show
{
// Retaining self is odd, but we do it to make this "fire and forget"
[self retain];

// We need to add it to the window, which we can get from the delegate
id appDelegate = [[UIApplication sharedApplication] delegate];
UIWindow *window = [appDelegate window];
[window addSubview:self.view];

// Make sure the alert covers the whole window
self.view.frame = window.frame;
self.view.center = window.center;

// "Pop in" animation for alert
[alertView doPopInAnimationWithDelegate:self];

// "Fade in" animation for background
[backgroundView doFadeInAnimation];
}

The next action method we write is the one that gets called by the two buttons on the alert. Regardless of which button was pushed, we want the text field to resign first responder status so that the keyboard disappears, and we want the alert to fade away. We're going to use implicit animations this time and then use performSelector:withObject:afterDelay: to trigger our private method that will remove the view from its superview. After that, we check sender's tag value to see which delegate method to notify.

- (IBAction)dismiss:(id)sender
{
[inputField resignFirstResponder];
[UIView beginAnimations:nil context:nil];
self.view.alpha = 0.0;
[UIView commitAnimations];

[self performSelector:@selector(alertDidFadeOut) withObject:nil afterDelay:0.5];

if (sender == self || [sender tag] == CustomAlertViewButtonTagOk)
[delegate CustomAlertView:self wasDismissedWithValue:inputField.text];
else
{
if ([delegate respondsToSelector:@selector(customAlertViewWasCancelled:)])
[delegate customAlertViewWasCancelled:self];
}

}

The viewDidUnload and dealloc are bog standard, so there's no point in discussing them. The next method after those is our "private" method. It does nothing more than remove the now invisible alert view from the view hierarchy and autoreleases self so that the view controller will be released at the end of the current run loop iteration. We don't want to use release because we really don't want the object disappearing while one of its method is executing:

- (void)alertDidFadeOut
{
[self.view removeFromSuperview];
[self autorelease];
}

Earlier, when we created the "pop in" animation, we specified self as the delegate. The next method we implement is called when the "pop in" animation completes by virtue of that fact. All we do here is make sure the keyboard is shown to the user and associated with our text field. We do all that simply by making the text field the first responder:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
[self.inputField becomeFirstResponder];
}

Finally, we implement one of the text field delegate methods so that when the user presses the return key on the keyboard, it dismisses the dialog.

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
[self dismiss:self];
return YES;
}

At this point, we're done. We can now use this custom alert view exactly the same way we use UIAlertView:

    CustomAlertView *alert = [[CustomAlertView alloc]init];
alert.delegate = self;
[alert show];
[alert release];


As I stated earlier, you can use this same technique to present anything you can build in Interface Builder, and the result will be a highly-reusable alert object.



14 comments:

Daniel said...

thanks again for such a great contribution to the community. fun to read and insightful!

cheers,
daniel

Joe Conway said...

Interesting approach.

As a UIAlertView is a UIView subclass, you can add subviews directly to it - including labels and text fields. Unfortunately, setting the frame of a UIAlertView has undefined behavior, but, you can get around that (in a somewhat silly way) by adding a bunch of \n characters to the UIAlertView's message property. This advantage to this approach is that it is only a few lines of code (and still does not use any private API).

Also, the way Apple does the gradient background is by using another window. This window is of type _UIAlertOverlayWindow, which of course, is private. However, mimicking the behavior is relatively straightforward. Subclass UIWindow and override its drawRect: method to draw the gradient. To get a window to appear "above" the status bar, you can send it the message setWindowLevel: with a value of UIWindowLevelStatusBar + 1.

And lastly, I would humbly advise against using images for interface elements that can be easily reproduced with a few lines of Core Graphics. Granted, these images are only totaling about 50k, but that can add up. (Not to mention it is much easier to distribute solutions without having to include images!)

Here is an (tab-mangled) example of the background in Core Graphics:
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
float comps[8] = {0, 0, 0, .3, 0, 0, 0, 0.6};
float locs[2] = {0, 1};
CGGradientRef gradient = CGGradientCreateWithColorComponents(space, comps, locs, 2);

float x = [self bounds].size.width / 2.0;
float y = [self bounds].size.height / 2.0;
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextDrawRadialGradient(ctx,
gradient,
CGPointMake(x, y), 0,
CGPointMake(x, y), 160,
kCGGradientDrawsAfterEndLocation);
CGColorSpaceRelease(space);
CGGradientRelease(gradient);

Jeff LaMarche said...

Joe:

Great info as always.

Honestly, I've done the subclass UIAlertView route and it's more trouble than it's worth. There are just many limitations, and accessing private methods and variables in a subclass used to be a gray area, but now seems pretty firmly in the "don't go there" category, so I think it's better off to do a workalike than to try to extend the existing class.

As for your comment about using images, however - I'm of a mixed mind. You're absolutely right that replacing nearly 50k of images with six lines of code is a win, but when you work with graphic designers, it's much easier to use images because you can work with placeholders that can be easily substituted in later, and in a modern world a few k (or even a few megs) of images in an application really isn't a big deal.

Coding CG code is tedious and doesn't lend itself well to the working with designers, at least those who aren't very technically savvy. For one-shots, I lean towards the least labor-intensive solution (dropping in an image), but for generic, reusable code, CG is definitely a good idea. In this particular case, you're right - the CG code is definitely a win, and if it's okay with you, I'll probably post a new version that uses your CG code (with attribution, of course).

Thomas Balthazar said...

Hello Jeff,

Thanks for your article.

In the dismiss: method you call alertDidFadeOut after a 0.5 sec delay.

Wouldn't it be an interesting alternative to use the animation delegate to make sure this method is called after the animation is finished, instead of guessing it will be finished after a short delay.

It may go like this :
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(alertDidFadeOut)];
(Full code here)

In the end, both methods lead to the same result, but I find the delegate approach a little cleaner.

What do you think?

Thanks again for your interesting articles.
Cheers,
Thomas.

Jeff LaMarche said...

Thomas:

Yep, that is actually a better solution. I tend to forget that implicit animations support a did end selector just like explicit ones.

Mike said...

I solved this problem a while ago using CG and allowing full styling of the alert view (Note: it uses Three20 styles and relies on some of the Three20 framework code) As the previous comments stated there is the extra UIWindow that gets managed in there as well.

Here is the code for the alertView: http://github.com/uprise78/three20-P31/blob/master/P31-Additions/Views/P31AlertView.m

Just about all the methods in UIAlertView are there including adding UITextFields and multiple buttons. I didn't turn it into a UITableView when the button count is too large yet but that might happen one day. It's a neat little package to rummage through to get some ideas from. By no means is the code perfect but there are some goodies in there.

Fritz Anderson said...
This comment has been removed by the author.
Fritz Anderson said...

I'm grateful for this example. It makes my life much easier.

I find that the view does not rotate. If the device is in landscape orientation when show: is called, shouldAutorotateToInterfaceOrientation: does get called on the view controller, and I always return YES, but the alert view always is oriented as for UIInterfaceOrientationPortrait.

The enclosing view is set in IB with fixed placement and free dimensions (and behaves as expected; the alert content view is set with fixed dimensions and fixed upper edge. I don't think that matters.

This is on an iPad, iOS 3.2, by the way.

Fritz Anderson said...

I now know what's going on. See QA 1688: A second view at the root of a UIWindow will not receive correct orientation service.

I corrected this by inserting the alert-view tree into subview 0 of the UIWindow. That took care of orientation.

However, in landscape — and this is early in the app's life cycle, but after a performSelector:..afterDelay:, the superview and window frames were still oriented as in portrait. I had to rotate the frame rectangle by hand if in landscape mode.

This strikes me as an outrageous kludge. Any ideas on how to do it right?

SEO Services Consultants said...

Nice information, many thanks to the author. It is incomprehensible to me now, but in general, the usefulness and significance is overwhelming. Thanks again and good luck! Web Design Company

rZz said...

jeff....
i was try your tutorial...
it was successfull to made a view like an alert view, but i have a problem when i implement it in my application. that view costumAlertView's orientation not fit into the ipad orientation, the costumAlertView's orientation is always landscape toward the ipad home button.

and then the method willAnimateSecondHalfOfRotationFromInterfaceOrientation not called...
can you help me to resolve it???

rZz said...

@fritz

i think my problem is same with yours..
can you describe what you do to resolve it??

Orion said...

This CustomAlertView does it all using Core Graphics stack:

https://github.com/Orion98MC/miOStuff

Regards,
Thierry

Devin said...

I am agree with you.
thanks again for such a great article.
Generic Viagra Online