Back to blog

Custom UIViewController Transitions

Update: As of iOS 7.0, interface transitions in landscape orientation are in a dire state. Read more about it. This article focuses on portrait-only transitions.

When teaching a new programming technique, there is a spectrum ranging from practice to theory. At one end, you teach only what you need to understand to implement a feature. At the other end, you teach the reasoning behind the API necessary to implement a feature. Too practical and you risk creating cargo-cult coders. Too theoretical and you risk alienating users of your API. It’s a tough balance to strike.

Of all of the new APIs introduced in iOS 7, perhaps the most confusing was the custom UIViewController transitions API. This is due mostly because the WWDC presentation leaned heavily toward the theoretical end of the spectrum. The problem is exacerbated by the lack of sample code illustrating how to use the custom view controller presentation API.

We’re here to fix that. The API itself isn’t that confusing – it just takes some experience getting your hands dirty. Let’s dive in.

Recall that UIViewController is the main unit of composition for application logic within iOS applications. View controllers are presented to users via navigation controllers, tab bar controllers, and modally. Before iOS 7, each of these presentations had predefined animations that were not customizable. Pushes onto a navigation controller’s stack moved from right to left. Selecting a different tab didn’t provide any animation. Modal presentations used one of a few pre-defined transitions (the default was a slide-up).

What’s more is that once a transition was complete, the presenting view controller was no longer at all visible (on the iPhone, at least). This made implementing custom modal views difficult.

iOS 7 introduces a new way to use a completely custom animation when transitioning from one view controller to another, whether it be a push onto a navigation controller stack, selecting a different tab, or a plain presentation. Additionally, the API allows you to present a view controller without necessarily obscuring the presenting controller. This makes faux popovers and alert views possible for the first time using UIViewControllers. Awesome!

A custom transition can either be interactive or non-interactive. We’re going to focus on the non-interactive type first because it’s a lot easier to implement. Remember that the goal of this article isn’t to explain the API – check out the WWDC video for a great explanation – the goal here is gain practical, hands-on experience.

Here’s the interaction we’re going to create. It’s nothing special – just a view appearing from the right edge of the screen. What is special is that we’re actually presenting a view controller, even though the presenting view controller remains visible.

So how do we accomplish this? The trick here is to create a new object called the animator that will be responsible for animating the presentation (and corresponding dismissal). When you present the view controller, set the modalPresentationStyle to UIModalPresentationCustom and set yourself as the transitionDelegate. Then implement the UIViewControllerTransitioningDelegate methods to vend the animator to the system.

You can do this in one of a few ways. We’re using a Storyboard file with a modal segue to the detail view controller, so we’ll implement the prepareForSegue:sender: method.

However, you could use the traditional presentation API if you’re not using Storyboards.

UIViewController *viewController = ...;
viewController.transitioningDelegate = self;
viewController.modalPresentationStyle = UIModalPresentationCustom;
[self presentViewController:viewController animated:YES completion:nil];

This code is for presenting a view controller modally. Similar techniques work with UINavigationControllers and UITabBarControllers. In those cases, simply comform to those class’ delegate protocols and implement the corresponding methods to vend an animator. In our examples, we’re going to use plain presentation methods.

Note that if you set the modal presentation style to custom, the system expects you to provide a non-nil transitioning delegate. You’ll receive a runtime warning if you don’t.

After defining the transitioningDelegate on the presented view controller, we’ll need to implement the UIViewControllerTransitioningDelegate methods to vend the animator.

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source {
   
   TLTransitionAnimator *animator = [TLTransitionAnimator new];
   animator.presenting = YES;
   return animator;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
   TLTransitionAnimator *animator = [TLTransitionAnimator new];
   return animator;
}

That’s really all there is to it. With only a few lines of code, we’ve invoked a custom transition to a new view controller. This is awesome because the presenting view controller is completely unaware of how the presentation will take place – there is a clear separation of concerns. This is also awesome because we can reuse the animator elsewhere in our application for the same presentation logic.

So what’s in the animator? The animator is just an NSObject subclass that conforms to the UIViewControllerAnimatedTransitioning protocol. The two required methods of this protocol define how long the animation from one view controller to the other will take, and the code to actually animate that transition.

When the transition itself happens, the animator is passed a transition context that holds information about the transition. This includes the “from” and “to” view controllers and a container view. This container view is where the animation actually takes place. You add both view controllers’ views to the container view, perform some transition animation, then tell the context that the transition has completed. It’s that simple.

Our animator takes care of both a presentation and a dismissal (the property that is set in our UIViewControllerTransitioningDelegate methods). Let’s take a look at the complete implementation.

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

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
   // Grab the from and to view controllers from the context
   UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
   UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
   
   // Set our ending frame. We'll modify this later if we have to
   CGRect endFrame = CGRectMake(80, 280, 160, 100);
   
   if (self.presenting) {
       fromViewController.view.userInteractionEnabled = NO;
       
       [transitionContext.containerView addSubview:fromViewController.view];
       [transitionContext.containerView addSubview:toViewController.view];
       
       CGRect startFrame = endFrame;
       startFrame.origin.x += 320;
       
       toViewController.view.frame = startFrame;
       
       [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
           fromViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
           toViewController.view.frame = endFrame;
       } completion:^(BOOL finished) {
           [transitionContext completeTransition:YES];
       }];
   }
   else {
       toViewController.view.userInteractionEnabled = YES;
       
       [transitionContext.containerView addSubview:toViewController.view];
       [transitionContext.containerView addSubview:fromViewController.view];
       
       endFrame.origin.x += 320;
       
       [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
           toViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic;
           fromViewController.view.frame = endFrame;
       } completion:^(BOOL finished) {
           [transitionContext completeTransition:YES];
       }];
   }
}

The first method is very straightforward – how long should the transition take? The next method is a little trickier. It’s passed the transition context then, depending on whether it’s presenting or dismissing, performs an animation to present or dismiss the detail view controller.

We’re using plain ol’ UIView block-based animations here. Nothing fancy. The only tricky thing is that the “to” and “from” view controllers change depending on whether you’re presenting or dismissing. That is to say, the presenting view controller is the “from” controller when presenting and the “to” controller when dismissing.

As you can see, it’s not a lot of code to implement a custom transition. Let’s take a look at a more complicated example: an interactive transition. These are trickier for a few reasons. First, you’ll usually want a way to present an interactive transition non-interactively, as well as dismiss it non-interactively. This lets users choose how they want to present or dismiss the content in the view controller. Additionally, the interactivity is tied to a gesture recognizer. Where does the code go to respond to that recognizer?

The answer is to subclass UIPercentDrivenInteractiveTransition, make it the animator, the transitioning delegate, and the gesture recognizer target. This is going to bundle all of your transitioning logic into one place. There’s a lot to unwind here, so let’s take it one step at a time.

First, our interactor is going to be initialized with a parent view controller. This is because the interactor itself is going to be responsible for presenting the new view controller in the gesture recognizer callback method.

-(id)initWithParentViewController:(UIViewController *)viewController {
   if (!(self = [super init])) return nil;
   
   _parentViewController = viewController;
   
   return self;
}

The interactor is the target of a screen edge pan gesture recognizer which we’ll set up in our presenting view controller’s viewDidLoad.

UIScreenEdgePanGestureRecognizer *gestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self.menuInteractor action:@selector(userDidPan:)];
gestureRecognizer.edges = UIRectEdgeLeft;
[self.view addGestureRecognizer:gestureRecognizer];

Our userDidPan: method looks like the following.

-(void)userDidPan:(UIScreenEdgePanGestureRecognizer *)recognizer {
   CGPoint location = [recognizer locationInView:self.parentViewController.view];
   CGPoint velocity = [recognizer velocityInView:self.parentViewController.view];
   
   if (recognizer.state == UIGestureRecognizerStateBegan) {
       // We're being invoked via a gesture recognizer – we are necessarily interactive
       self.interactive = YES;
       
       // The side of the screen we're panning from determines whether this is a presentation (left) or dismissal (right)
       if (location.x < CGRectGetMidX(recognizer.view.bounds)) {
           self.presenting = YES;
           TLMenuViewController *viewController = [[TLMenuViewController alloc] initWithPanTarget:self];
           viewController.modalPresentationStyle = UIModalPresentationCustom;
           viewController.transitioningDelegate = self;
           [self.parentViewController presentViewController:viewController animated:YES completion:nil];
       }
       else {
           [self.parentViewController dismissViewControllerAnimated:YES completion:nil];
       }
   }
   else if (recognizer.state == UIGestureRecognizerStateChanged) {
       // Determine our ratio between the left edge and the right edge. This means our dismissal will go from 1...0.
       CGFloat ratio = location.x / CGRectGetWidth(self.parentViewController.view.bounds);
       [self updateInteractiveTransition:ratio];
   }
   else if (recognizer.state == UIGestureRecognizerStateEnded) {
       // Depending on our state and the velocity, determine whether to cancel or complete the transition.
       if (self.presenting) {
           if (velocity.x > 0) {
               [self finishInteractiveTransition];
           }
           else {
               [self cancelInteractiveTransition];
           }
       }
       else {
           if (velocity.x < 0) {
               [self finishInteractiveTransition];
           }
           else {
               [self cancelInteractiveTransition];
           }
       }
   }
}

Quite a lot there. Don’t worry, we’re going to go through it all. The most important thing to note is that the gesture recognizer code does not implement any animation code.

When our recognizer begins, we present (or dismiss) the view controller. When the recognizer changes, we update the percent complete on self. Finally, when the recognizer finishes, we decided whether to complete or cancel the transition depending on the last direction of the gesture recognizer. It’s a lot of code, but it’s all fairly straightforward.

Notice that when we present the new view controller, we set self as the transition delegate. When prompted to vend an animator, we’ll also return self.

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

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

There are two more methods to provide an interactor for the interactive transition. These methods are called after the previous methods. We’re going to return self if we’re interactive and nil if we’re not.

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator {
   if (self.interactive) {
       return self;
   }
   
   return nil;
}

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
   if (self.interactive) {
       return self;
   }
   
   return nil;
}

The next methods are copied almost directly from the first example. We need to provide animations to non-interactively present and dismiss the view controller.

What’s really interesting is the interactor method in the UIViewControllerInteractiveTransitioning protocol. We’ll implement this method to begin our interactive transition. Then we’ll override the UIPercentDrivenInteractiveTransition methods to update our transition, then finally to complete or cancel the transition.

The startInteractiveTransition: method sets up the container view with the “to” and “from” view controllers’ views. It’s important which order you add these in so the correct view is “on top.”

-(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
   self.transitionContext = transitionContext;
   
   UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
   UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
   
   CGRect frame = [[transitionContext containerView] bounds];
   
   if (self.presenting)
   {
       // The order of these matters – determines the view hierarchy order.
       [transitionContext.containerView addSubview:fromViewController.view];
       [transitionContext.containerView addSubview:toViewController.view];
       
       frame.origin.x -= CGRectGetWidth([[transitionContext containerView] bounds]);
   }
   else {
       [transitionContext.containerView addSubview:toViewController.view];
       [transitionContext.containerView addSubview:fromViewController.view];
   }
   
   toViewController.view.frame = frame;
}

Next we need to update the position of the menu view controller depending on the transition’s percent complete.

- (void)updateInteractiveTransition:(CGFloat)percentComplete {
   id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
   
   UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
   UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
   
   // Presenting goes from 0...1 and dismissing goes from 1...0
   CGRect frame = CGRectOffset([[transitionContext containerView] bounds], -CGRectGetWidth([[transitionContext containerView] bounds]) * (1.0f - percentComplete), 0);
   
   if (self.presenting)
   {
       toViewController.view.frame = frame;
   }
   else {
       fromViewController.view.frame = frame;
   }
}

Finally, the code to complete or cancel the transition is below. It’s critically important that no matter what, completeTransition: is called on the transition context that was passed in startInteractivetransition:. We’ll call this method in the completion block of our animation.

- (void)finishInteractiveTransition {
   id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
   
   UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
   UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
   
   if (self.presenting)
   {
       CGRect endFrame = [[transitionContext containerView] bounds];
       
       [UIView animateWithDuration:0.5f animations:^{
           toViewController.view.frame = endFrame;
       } completion:^(BOOL finished) {
           [transitionContext completeTransition:YES];
       }];
   }
   else {
       CGRect endFrame = CGRectOffset([[transitionContext containerView] bounds], -CGRectGetWidth([[self.transitionContext containerView] bounds]), 0);
       
       [UIView animateWithDuration:0.5f animations:^{
           fromViewController.view.frame = endFrame;
       } completion:^(BOOL finished) {
           [transitionContext completeTransition:YES];
       }];
   }
}

- (void)cancelInteractiveTransition {
   id<UIViewControllerContextTransitioning> transitionContext = self.transitionContext;
   
   UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
   UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
  
   if (self.presenting)
   {
       CGRect endFrame = CGRectOffset([[transitionContext containerView] bounds], -CGRectGetWidth([[transitionContext containerView] bounds]), 0);
       
       [UIView animateWithDuration:0.5f animations:^{
           toViewController.view.frame = endFrame;
       } completion:^(BOOL finished) {
           [transitionContext completeTransition:NO];
       }];
   }
   else {
       CGRect endFrame = [[transitionContext containerView] bounds];
       
       [UIView animateWithDuration:0.5f animations:^{
           fromViewController.view.frame = endFrame;
       } completion:^(BOOL finished) {
           [transitionContext completeTransition:NO];
       }];
   }
}

We’ve driven this transition completely using plain, boring UIView block animations. What’d be super-cool is to use the new UIKit Dynamics to drive the animations. That’s beyond the scope of this tutorial, but you’ll find the code for it in the TLMenuDynamicInteractor. Just set the USE_UIKIT_DYNAMICS C macro to YES to use the dynamic interactor instead.

The most important thing to note about the dynamic version of the interactor is that, unlike our last post’s example of driving an attachment behaviour with a gesture recognizer, the gesture recognizer callback method does not touch the attachment behaviour directly.

All of the sample code we’ve discussed is openly available on GitHub. Check it out and let us know what you think.

As we’ve seen, custom UIViewController transitions are not difficult. You’re now armed to make super-awesome animations and we’re excited to see what you come up with.

Ash Furrow More posts by Ash Furrow