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:
We also need a background image for the alert view. Here's one I hacked out in
Pixelmator
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:
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:
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{
[self retain];
id appDelegate = [[UIApplication sharedApplication] delegate];
UIWindow *window = [appDelegate window];
[window addSubview:self.view];
self.view.frame = window.frame;
self.view.center = window.center;
[alertView doPopInAnimationWithDelegate:self];
[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
{
[self retain];
id appDelegate = [[UIApplication sharedApplication] delegate];
UIWindow *window = [appDelegate window];
[window addSubview:self.view];
self.view.frame = window.frame;
self.view.center = window.center;
[alertView doPopInAnimationWithDelegate:self];
[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.