Custom Transitions on iOS

This article is the first in a series that shows how to customize view controller transitions on iOS. In part one, the focus is on creating custom animated (non-interactive) transitions.

When using typical iOS apps we navigate from screen to screen frequently. Previously, if you wanted to do anything other than the standard transition animations, you were on your own, but in iOS 7 Apple has provided a new API to let us customize these animations.

iOS provides several built-in types of transitions. Navigation controllers push and pop to navigate through an information hierarchy, tab bar controllers switch between sections by changing tabs, and any view controller can present and dismiss another view controller modally for specific tasks.

API Introduction

Every custom transition involves three main objects:

  • The from view controller (the one that’s going away)
  • The to view controller (the one that’s appearing)
  • An animation controller

Starting a custom transition works the same way as before the transition was customized. For push and pop, it means calling one of the push-, pop-, or set- methods on UINavigationController to modify its stack of view controllers. For changing tabs, it means setting the selectedIndex or selectedViewController property on a UITabBarController. For a modal transition, it means calling ‑[UIViewController presentViewController:animated:completion:] or ‑[UIViewController dismissViewControllerAnimated:completion:]. In each case, this step determines which view controller will be the “from view controller” and which one will be the “to view controller.”

To use a custom transition, you need to say which object should play the role of the animation controller. For me, this was the most confusing part of setting up a custom animated transition because each type of transition looks for the animation controller in a different place. The following table shows how to provide the animation controller for each case. Just remember that you always return the animation controller from a delegate method.

Transition Animation controller provided by
Push and Pop The navigation controller’s delegate implements ‑navigationController:animationControllerForOperation:fromViewController:toViewController:
Changing Tabs The tab bar controller’s delegate implements ‑tabBarController:animationControllerForTransitionFromViewController:toViewController:
Present The presented view controller’s transitioningDelegate implements ‑animationControllerForPresentedController:presentingController:sourceController:. The presented view controller’s ‑modalPresentationStyle must be set to UIModalPresentationCustom before the transition is started.
Dismiss The presented view controller’s transitioningDelegate implements ‑animationControllerForDismissedController: The presented view controller’s ‑modalPresentationStyle must be set to UIModalPresentationCustom before the transition is started.

The animation controller can be any object that conforms to the UIViewControllerAnimatedTransitioning protocol. The protocol defines two required methods. One provides the animation duration, and the other performs the animation. Each of these methods is passed a context when it is called. The context provides access to the information and objects you need to create your custom transition. Here are some of the highlights:

  • The from view controller
  • The to view controller
  • The initial and final frames for the from and to view controllers’ views
  • The container view, which, according to the docs, “acts as the superview for the views involved in the transition.”

Important: The context also implements ‑completeTransition: which you must call once your custom transition is finished.

That is everything you need to know to make a custom transition. Let’s try some examples!

Examples

All of these examples are available on GitHub, so clone the repo and experiment with it as you follow along.

All three examples use TWTExampleViewController either directly or by subclassing. All it does on its own is set the background color for its view and make it so that you can tap anywhere inside of it to end the example and go back to the main menu.

Push and pop with flips

In this example, the goal is to make push and pop use flip animations instead of the standard slide animation. I started by setting up a navigation controller with an instance of TWTPushExampleViewController as the root. TWTPushExampleViewController adds a button to the right side of the navigation bar titled “Push”. When it is tapped, a new instance of TWTPushExampleViewController is pushed on to the navigation stack:

- (void)pushButtonTapped
{
    TWTPushExampleViewController *viewController = [[TWTPushExampleViewController alloc] init];
    viewController.delegate = self.delegate;
    [self.navigationController pushViewController:viewController animated:YES];
}

The setup for the navigation controller happens in TWTExamplesListViewController (the view controller that runs the main menu for the demo app). Notice that it sets itself as the navigation controller’s delegate:

- (void)presentPushExample
{
    TWTPushExampleViewController *viewController = [[TWTPushExampleViewController alloc] init];
    viewController.delegate = self;

    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
    navigationController.delegate = self;

    [self presentViewController:navigationController animated:YES completion:nil];
}

This means that when a navigation controller transition is about to start, TWTExamplesListViewController will receive the delegate message and have an opportunity to return an animation controller. For this transition, I am using an instance of TWTSimpleAnimationController which is just a wrapper around +[UIView transitionFromView:toView:duration:options:completion:]:

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                  animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController *)fromVC
                                                 toViewController:(UIViewController *)toVC
{
    TWTSimpleAnimationController *animationController = [[TWTSimpleAnimationController alloc] init];
    animationController.duration = 0.5;
    animationController.options = (  operation == UINavigationControllerOperationPush
                                   ? UIViewAnimationOptionTransitionFlipFromRight
                                   : UIViewAnimationOptionTransitionFlipFromLeft);
    return animationController;
}

If the transition is a push, I use a flip from the right, otherwise, I use a flip from the left.

Here’s what the implementation of TWTSimpleAnimationController looks like:

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return self.duration;
}


- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
    [toViewController.view layoutIfNeeded];

    [UIView transitionFromView:fromViewController.view
                        toView:toViewController.view
                      duration:self.duration
                       options:self.options
                    completion:^(BOOL finished) {
                        [transitionContext completeTransition:YES];
                    }];
}

Remember, these two methods are part of the UIViewControllerAnimatedTransitioning protocol. They’re called by UIKit when it’s time for the animation controller to run the custom transition.

Here are a couple things to notice about ‑animateTransition::

  • The from view controller, to view controller, and final frame for the to view controller’s view are all extracted from the transition context. There are other pieces of information that are available, but in this case, not all of them were needed.
  • +[UIView transitionFromView:toView:duration:options:completion:] takes care of adding and removing the view controllers’ views to the view hierarchy. In a later example, I will show a case in which it is done manually.
  • In the transition’s completion block, I call [transitionContext completeTransition:YES]; to tell the system that the transition is over. If you forget to do this, you will not be able to interact with the app. If that happens, check this first.

That’s all there is to it! Here are some ideas for things to try:

  • Change the animation duration and notice how it affects the animation in the navigation bar.
  • Change the animation options from flips to page curls.
  • Find a way to let each view controller in the navigation stack specify its own animation controller. See the recommended patterns section at the end of the article for a suggested approach.

Change tabs using a cross dissolve

This example should be familiar. It uses the same ideas as before, but with a tab bar controller instead of a navigation controller.

Here’s the setup in TWTExamplesListViewController:

- (void)presentTabsExample
{
    NSMutableArray *viewControllers = [[NSMutableArray alloc] init];

    for (NSUInteger i=0; i<3; i++) {
        TWTChangingTabsExampleViewController *viewController = [[TWTChangingTabsExampleViewController alloc] init];
        viewController.delegate = self;
        viewController.index = i;
        [viewControllers addObject:viewController];
    }

    UITabBarController *tabBarController = [[UITabBarController alloc] init];
    [tabBarController setViewControllers:viewControllers animated:NO];
    tabBarController.delegate = self;

    [self presentViewController:tabBarController animated:YES completion:nil];
}

The index in the TWTChangingTabsExampleViewController is just a way to customize the tab for each view controller. Just like before, the TWTExamplesListViewController is the delegate and will provide the animation controller when it is time to change tabs:

- (id<UIViewControllerAnimatedTransitioning>)tabBarController:(UITabBarController *)tabBarController
           animationControllerForTransitionFromViewController:(UIViewController *)fromVC
                                             toViewController:(UIViewController *)toVC
{
    TWTSimpleAnimationController *animationController = [[TWTSimpleAnimationController alloc] init];
    animationController.duration = 0.5;
    animationController.options = UIViewAnimationOptionTransitionCrossDissolve;
    return animationController;
}

Look familiar? That’s all there is to it.

Present an Overlay

One of my favorite uses of custom transitions is the ability to present overlay view controllers. Previously, if you wanted to mimic the behavior of one of the social sharing sheets or do anything else that required the presenting view controller to remain visible behind the presented content, you were left to choose between a number of unfortunate options (adding views directly to the window was the most common approach I had seen). Happily, this is no longer necessary.

The key reason why this approach works is that when the presented view controller’s ‑modalPresentationStyle is set to UIModalPresentationCustom, the presenting view controller’s view is not automatically removed from the view hierarchy. To build an overlay, it can simply be left as-is and the presented view controller’s view can be displayed above it.

Here’s the setup in TWTExamplesListViewController:

- (void)presentPresentExample
{
    TWTOverlayExampleViewController *viewController = [[TWTOverlayExampleViewController alloc] init];
    viewController.delegate = self;
    viewController.modalPresentationStyle = UIModalPresentationCustom;
    viewController.transitioningDelegate = self;

    [self presentViewController:viewController animated:YES completion:nil];
}

The two important things to note here are that ‑modalPresentationStyle is set to UIModalPresentationCustom, and the presented view controller’s ‑transitioningDelegate is set to the TWTExamplesListViewController. When the present starts, TWTExamplesListViewController receives the transitioning delegate message:

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source
{
    return [[TWTPresentAnimationController alloc] init];
}

As you can see, I created a custom animation controller class for this example. I am going to focus on the implementation of ‑animateTransition: since it is the part that is different from before.

To start off, I extract the to view controller (the presented view controller) and the container view from the transition context.

UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

UIView *containerView = [transitionContext containerView];

The container view is the superview for the from and to view controllers’ views during the transition. Initially, the to view controller’s view has not been added to the container view, and its frame has not been set. I am setting the frame directly here, but you could also use auto layout.

CGRect frame = containerView.bounds;
frame = UIEdgeInsetsInsetRect(frame, UIEdgeInsetsMake(40.0, 40.0, 200.0, 40.0));

toViewController.view.frame = frame;

[containerView addSubview:toViewController.view];

For this transition, I want the view to pop on to the screen. To do this, I animated the view’s scale from 0.3 to 1 using a UIView spring animation.

As an aside, starting from a scale value greater than 0 and simultaneously animating the view’s alpha from 0 to 1 lets you make the animation faster than you would want to make it if you simply scaled from 0 to 1. Try removing the alpha animation and starting the scale animation from 0 to compare.

toViewController.view.alpha = 0.0;
toViewController.view.transform = CGAffineTransformMakeScale(0.3, 0.3);

NSTimeInterval duration = [self transitionDuration:transitionContext];

[UIView animateWithDuration:duration / 2.0 animations:^{
    toViewController.view.alpha = 1.0;
}];

CGFloat damping = 0.55;

[UIView animateWithDuration:duration delay:0.0 usingSpringWithDamping:damping initialSpringVelocity:1.0 / damping options:0 animations:^{
    toViewController.view.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
    [transitionContext completeTransition:YES];
}];

In the completion block, I call ‑completeTransition:, and that’s it! Now for dismiss…

When the dismiss starts, TWTExamplesListViewController receives the transitioning delegate message:

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [[TWTDismissAnimationController alloc] init];
}

Another custom animation controller. Let’s take a look at ‑animateTransition::

UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

NSTimeInterval duration = [self transitionDuration:transitionContext];

[UIView animateWithDuration:3.0 * duration / 4.0
                      delay:duration / 4.0
                    options:UIViewAnimationOptionCurveEaseIn
                 animations:^{
                     fromViewController.view.alpha = 0.0;
                 }
                 completion:^(BOOL finished) {
                     [fromViewController.view removeFromSuperview];
                     [transitionContext completeTransition:YES];
                 }];

[UIView animateWithDuration:2.0 * duration
                      delay:0.0
     usingSpringWithDamping:1.0
      initialSpringVelocity:-15.0
                    options:0
                 animations:^{
                     fromViewController.view.transform = CGAffineTransformMakeScale(0.3, 0.3);
                 }
                 completion:nil];

As you can see, the idea is that it is the reverse of the present animation. The important difference to notice is that the from view controller’s view is removed from the view hierarchy before the transition is complete.

Before going on, try making some changes to the dismiss animation.

Recommended Patterns

By now, you have probably noticed that this API relies heavily on protocols and delegate methods. Because of this, it is easy to end up with a custom transition implementation that is disorganized and impossible to reuse. Here are some patterns that help me keep things clean:

  1. Use dedicated objects for animation controllers

    While it is possible for a view controller to double as the animation controller, doing so will almost certainly reduce the reusability of your custom transition. Besides, view controllers are already notorious for doing too much. You can create reusable animation controllers by simply creating NSObject subclasses that conform to UIViewControllerAnimatedTransitioning and then returning instances of them when needed.

    We did this in Toast with TWTSimpleAnimationController which is easy to reuse and include as a CocoaPod.

  2. Prevent UINavigationControllerDelegate from needing to know about specific transitions

    Returning the animation controller from the navigation controller’s delegate is fine as long as you want all push and pop transitions to work the same way. In some cases, though, you might only want to customize a single push or pop.

    One messy way to do it would be to temporarily change the navigation controller’s delegate to an object that knows about the transition. Another messy way to do it would be to make the navigation controller’s delegate have logic specific to certain combinations of from and to view controllers. Clearly, neither of those approaches is a good option.

    A pattern that solves this problem cleanly is to add properties to UIViewController called ‑pushAnimationController and ‑popAnimationController. Then the navigation controller’s delegate can be made to return the animation controller as specified by the pushed or popped view controller. This allows the navigation controller’s delegate to remain generic and avoids the need to change which object is the delegate. TWTNavigationControllerDelegate implements this pattern and is also included in Toast.

That wraps up this introduction to creating custom animated view controller transitions. Be sure to take a look at our view controller transitions module in Toast, and stay tuned for part two, which will cover custom interactive transitions.

Tweet about this on Twitter