Transcending UIAlertView on iOS 7

RYO small floating view containing any interface you like

Of all the new features and APIs that iOS 7 provides to developers, none, in my opinion, is as important from a user interface perspective as custom view controller transitions, the ability to insert your own animation when a view controller’s view takes over the screen. Thus:

  • When a tab bar controller’s child is selected, you can now animate the change.
  • When a view controller is pushed onto a navigation controller’s stack, you are no longer confined to the traditional “slide in from the right” animation.
  • When a presented view controller’s view appears or is dismissed, you are no longer confined to a choice of the four UIModalTransitionStyle animations.

In the third case — a presented view controller — iOS 7 introduces a further innovation: You can position the presented view wherever you like, including the possibility of only partially covering the original interface.

In other words, the presented view controller’s view can float on top of, and partially reveal, the original view controller’s view; the user sees the views of both view controllers, one in front of the other.

This is particularly significant on the iPhone. In the past, on the iPhone, a presented view controller’s view was always fullscreen, completely covering — in reality, replacing — the original view controller’s view. In iOS 7, that’s no longer the case; a presented view controller’s view can cover just part of the screen, and the original view controller’s view is not removed.

To demonstrate the power of this brave new world, I’ll show how you can take advantage of this feature to escape the tyranny of UIAlertView, by rolling your own small floating view, which appears and disappears from the screen similarly to a UIAlertView — but, unlike a real UIAlertView, this view can contain any interface you like.

(NOTE: This example is based on a section of my book, Programming iOS 7. My github site lets you download all the example code from the book, but I’ve also created a special demo project for this article: https://github.com/mattneub/custom-alert-view-iOS7.)

Here’s a screen shot, showing what we’re going to create:

running

  • On the left is our app’s basic interface, consisting of just a “Show Custom Alert View” button (at the top) and an image view. The idea is merely to provide a sufficiently busy and distinctive interface for our custom alert view to float over.
  • On the right, our custom alert view has appeared, and floats in front of the original interface. Note that, like a UIAlertView, it darkens the rest of the screen slightly, and causes the “Show Custom Alert View” button (now in the background) to dim its title. But this is clearly not a normal UIAlertView: It contains an image view! And a switch control!! Those are things that no ordinary UIAlertView can contain. The point here, clearly, is that you can put anything into this view! We are transcending the limitations of UIAlertView.

All the work here is done by a single view controller class, which I’ll call CustomAlertViewController, together with an eponymous .xib file, CustomAlertViewController.xib. Here’s a screen shot of the .xib file:

xib

There are two important views here:

  • The outer view, the size of the iPhone screen, is our view controller’s main view (its view). Its background color is a somewhat transparent light grey. This is the “shadow view” whose job is to darken the screen behind our alert view.
  • In the middle is the smaller subview that will become our actual alert view. CustomAlertViewController has an outlet property pointing to it, called alertView.

Here is CustomAlertViewController’s viewDidLoad implementation; it finishes the configuration of self.alertView, giving it a blue border and rounded corners:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.alertView.layer.borderColor = [UIColor blueColor].CGColor;
    self.alertView.layer.borderWidth = 2;
    self.alertView.layer.cornerRadius = 8;
}

Here is CustomAlertViewController’s initializer:

-(id)initWithNibName:(NSString *)nibNameOrNil 
              bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil 
                           bundle:nibBundleOrNil];
    if (self) {
        self.modalPresentationStyle = UIModalPresentationCustom;
        self.transitioningDelegate = self;
    }
    return self;
}

That code sets two UIViewController properties in ways that are new in iOS 7. Our modalPresentationStyle is UIModalPresentationCustom; that means we’re going to supply a custom transition animation. And our transitioningDelegate points to an object that will reveal where the custom transition animation is to come from.

So now the user comes along and taps the “Show Custom Alert View” button in the main interface. This causes a CustomAlertViewController instance to be created and presented:

- (IBAction) doButton: (id) sender {
    UIViewController* vc = [CustomAlertViewController new];
    [self presentViewController:vc animated:YES completion:nil];
}

Our CustomAlertViewController is being presented, with animation; thus, its modalPresentationStyle and transitioningDelegate are consulted. The transitioningDelegate is self, the CustomAlertViewController itself; in this role (adopting the UIViewControllerTransitioningDelegate protocol), the CustomAlertViewController must implement two methods, each pointing to an object that will provide the actual custom transition animation. To keep everything self-contained, that object, once again, is self:

-(id<UIViewControllerAnimatedTransitioning>)
        animationControllerForPresentedController:
            (UIViewController *)presented 
        presentingController:
            (UIViewController *)presenting 
        sourceController:
            (UIViewController *)source {
    return self;
}

-(id<UIViewControllerAnimatedTransitioning>)
        animationControllerForDismissedController:
            (UIViewController *)dismissed {
    return self;
}

Since we returned self from those methods, the runtime turns to CustomAlertViewController one more time (adopting the UIViewControllerAnimatedTransitioning protocol) for the details of the custom transition animation. First, we have to implement a method that reveals the duration of the animation:

-(NSTimeInterval)transitionDuration:
        (id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.25;
}

Finally, here comes the actual custom transition animation:

-(void)animateTransition:
        (id<UIViewControllerContextTransitioning>)transitionContext {
    // ...
}

I’ll fill in the contents of animateTransition: in stages. First, note the incoming parameter, transitionContext. This object, supplied by the runtime, knows some very important things that we, too, need to know:

  • The two view controllers involved in the transition.
  • The special container view inside which the views of those two view controllers will appear. Think of this container view as the stage on which the action will unfold.

So, our first step, inside animateTransition:, is to query the transitionContext to get that information:

UIViewController* vc1 =
    [transitionContext viewControllerForKey:
        UITransitionContextFromViewControllerKey];
UIViewController* vc2 =
    [transitionContext viewControllerForKey:
        UITransitionContextToViewControllerKey];
UIView* con = [transitionContext containerView];
UIView* v1 = vc1.view;
UIView* v2 = vc2.view;

We now know the two view controllers and their views, along with the container view.

We supplied self as the UIViewControllerAnimatedTransitioning object twice, both when we are being presented and when we are being dismissed. We must distinguish those two cases. That’s easy: When we are being presented, the second view controller (vc2) will be self, and its view (v2) is our view (self.view).

I’ll deal with each case in turn. When we are being presented, the container view already contains the first view controller’s view (v1). Our job is to put the second view controller’s view (v2, our self.view) into the container view (con), putting it wherever we like and animating it however we like. There is just one further requirement: when we are finished, we must send the transitionContext a message (completeTransition:) to tell it that the animation is over.

In our case, since our view is the surrounding “shadow view”, its frame will be the same as the frame of the original view behind it. To emphasize the arrival of the UIAlertView, we will animate our view from invisible to visible, and we will also use a scale transform to animate our alertView from large to normal, in imitation of a real UIAlertView:

if (vc2 == self) { // presenting
    [con addSubview:v2];
    v2.frame = v1.frame;
    self.alertView.transform = CGAffineTransformMakeScale(1.6,1.6);
    v2.alpha = 0;
    v1.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
    [UIView animateWithDuration:0.25 animations:^{
        v2.alpha = 1;
        self.alertView.transform = CGAffineTransformIdentity;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES];
    }];
} 

So much for presentation! Now let’s talk about dismissal. When the user taps the OK button in our alert view, we dismiss ourselves:

- (IBAction) doDismiss: (id) sender {
    [self.presentingViewController 
        dismissViewControllerAnimated:YES completion:nil];
}

Again, our animateTransition: method will be called to provide the transition animation. When we are being dismissed, the view controllers are the other way around: the second view controller (vc2) is the original view; the first view controller (vc1) is self, and its view (v1) is our view (self.view). We animate the disappearance of our view, but we do not have to remove it from the container view; the runtime will do that for us:

else { // dismissing
    [UIView animateWithDuration:0.25 animations:^{
        self.alertView.transform = CGAffineTransformMakeScale(0.5,0.5);
        v1.alpha = 0;
    } completion:^(BOOL finished) {
        v2.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic;
        [transitionContext completeTransition:YES];
    }];
}

Observe that in both cases we also alter the tintAdjustmentMode of the original view, just as a real UIAlertView does.

That’s all there is to it! Some final notes:

  • The example works no matter whether our alert view is summoned with the iPhone in portrait orientation or in landscape orientation. It even works if the device is rotated while the alert view is showing. That’s because our view controller is an ordinary view controller; it permits rotation, and therefore both the original view and our view (the “shadow view”, containing the alert view) are rotated and resized in the usual way. Constraints (created in the .xib file) keep the alert view centered in the shadow view.
  • A good thing to do in viewDidLoad is to attach a UIMotionEffectGroup to self.alertView, so that it appears to float with parallax over everything else, just like a real UIAlertView.

I hope that this example inspires you to create your own custom alert views. No longer do you have to wrestle with UIAlertView; in fact, you might never need to use UIAlertView ever again. Instead, you are free to create your own view and make it act like a UIAlertView. Feel free to tweak the example, in your own code, to make your view look and act more — or less! — like a real UIAlertView. The possibilities are endless!

tags: , , ,