Others Talk, We Listen.

iOS 7 Tutorial Series: Custom Navigation Transitions & More

by Tyler Tillage on Oct 18, 2013

Up until now, iOS developers seeking to customize navigation controller animations for pushing and popping view controllers have been limited to either subclassing UINavigationController or manually overlaying a custom animation. As of iOS 7.0 Apple has introduced new tools to give developers greater flexibility in dealing with UIViewController-managed state transitions:

  • New UIView block animation methods
  • New protocols and the concept of Animation Controllers
  • Interaction Controllers and Transition Coordinators
  • New animation-related helper methods

I've included a sample app that showcases some of the techniques I'll cover in this article. For a quick model of all the things you can do with iOS 7's new UIViewController transition APIs, download the app here.

New UIView Block Animation Methods

UIView's block animation methods introduced powerful new functionality with the release of iOS 4, however there are still situations that require us to fall back to using Core Animation explicitly. Fortunately, Apple has added 2 new block-based methods to our arsenal to make those situations even less frequent.

Keyframe Animations

iOS 7 give us an Objective-C wrapper around Core Animation's CAKeyframeAnimation which composes nicely with other UIView animation methods.

[UIView animateKeyframesWithDuration:duration delay:delay options:options animations:^{
    [UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
        //1st Animation
    }];
    [UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
        //2nd Animation
    }];
} completion:^(BOOL finished) {
    //Completion Block
}];

The new animateKeyframesWithDuration is to CAKeyframeAnimation just as animateWithDuration is to CABasicAnimation. Just add keyframes within the block, passing for each the relative start time and duration as a decimal percentage of the entire animation's duration. Note that the keyframes don't need to stack up one at a time -- you can have multiple keyframes within the animation running simultaneously.

Here's a quick example: below are the keyframes I use to dismiss my modal options view controller in the sample app.

[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.15 animations:^{
    //90 degrees (clockwise)
    snapshot.transform = CGAffineTransformMakeRotation(M_PI * -1.5);
}];
[UIView addKeyframeWithRelativeStartTime:0.15 relativeDuration:0.10 animations:^{
    //180 degrees
    snapshot.transform = CGAffineTransformMakeRotation(M_PI * 1.0);
}];
[UIView addKeyframeWithRelativeStartTime:0.25 relativeDuration:0.20 animations:^{
    //Swing past, ~225 degrees
    snapshot.transform = CGAffineTransformMakeRotation(M_PI * 1.3);
}];
[UIView addKeyframeWithRelativeStartTime:0.45 relativeDuration:0.20 animations:^{
    //Swing back, ~140 degrees
    snapshot.transform = CGAffineTransformMakeRotation(M_PI * 0.8);
}];
[UIView addKeyframeWithRelativeStartTime:0.65 relativeDuration:0.35 animations:^{
    //Spin and fall off the corner
    //Fade out the cover view since it is the last step
    CGAffineTransform shift = CGAffineTransformMakeTranslation(180.0, 0.0);
    CGAffineTransform rotate = CGAffineTransformMakeRotation(M_PI * 0.3);
    snapshot.transform = CGAffineTransformConcat(shift, rotate);
    _coverView.alpha = 0.0;
}];

The view spins clockwise around its bottom left corner as if gravity was switched on, falling off of its anchor point on the last keyframe.

Spring Animations

Another new animation block method in iOS 7 features built-in spring physics. Apple has always recommended that animations emulate real life physics as much as possible, and having a view travel slightly farther than its target position and then snap back like a spring is one popular way of achieving this. With the new spring animation block capturing these kinds of real physics behaviors is easier than ever.

[UIView animateWithDuration:duration delay:delay usingSpringWithDamping:damping initialSpringVelocity:velocity options:options animations:^{
    //Animations
} completion:^(BOOL finished) {
    //Completion Block
}];

As the damping value approaches 0.0 the spring becomes more bouncy. A value of 1.0 makes it smoothly decelerate without overshooting.

A value of 1.0 for an initial spring velocity corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5.

In the sample app I used a spring animation to slide my modal options view in from the bottom of the screen. A damping ratio of 0.8 and an initial spring velocity of 1.0 causes the view to travel roughly 15 points past its destination and then slowly come to rest. A damping ratio of 0.6 or less causes the view to overshoot higher, then bounce back and overshoot from the other direction before coming to rest.

Don't confuse this with iOS 7's new UIKit Dynamics engine - this API is a separate beast. It's meant to be a drop-in replacement for standard UIView animation blocks, giving us a small dose of physics in a lightweight package.

Customizing UIViewController Transitions

Now let's get into the good stuff. Not only did Apple give developers new types of animations, the iOS 7.0 SDK also expands the places you can use them. The following UIViewController-managed transitions can now be easily replaced with a custom animation:

  • UIViewController
    • presentViewController
  • UITabBarController
    • setSelectedViewController
    • setSelectedIndex
  • UINavigationController
    • pushViewController
    • popViewController
    • setViewControllers

In the sample app I chose to create a host of situations that involve these transitions and the use of the new spring and keyframe block methods. Now that you understand how to use the new block methods, let's go over how to plug them into the transitions listed above.

The Core: Animation Controllers

So how do we tell a view controller to use a custom animation while still leveraging its view hierarchy management? For this we have a new protocol, UIViewControllerAnimatedTransitioning, which wraps our animations. Any object which conforms to this protocol is called an Animation Controller in Apple parlance.

Since we're working with a protocol our custom animation code can reside anywhere, whether you want to create your own class to manage it or just make your UIViewController conform to UIViewControllerAnimatedTransitioning. Since I'm working with a number of different animations I chose to make a class for each animation, inheriting from the custom BaseAnimation class which defines common properties and helper methods.

Let's take a look at the first animation, a simple scale effect used for UINavigationController transitions:

-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    //Get references to the view hierarchy
    UIView *containerView = [transitionContext containerView];
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
 
    if (self.type == AnimationTypePresent) {
        //Add 'to' view to the hierarchy with 0.0 scale
        toViewController.view.transform = CGAffineTransformMakeScale(0.0, 0.0);
        [containerView insertSubview:toViewController.view aboveSubview:fromViewController.view];
 
        //Scale the 'to' view to to its final position
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            toViewController.view.transform = CGAffineTransformMakeScale(1.0, 1.0);
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:YES];
        }];
    } else if (self.type == AnimationTypeDismiss) {
        //Add 'to' view to the hierarchy
        [containerView insertSubview:toViewController.view belowSubview:fromViewController.view];
 
        //Scale the 'from' view down until it disappears
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            fromViewController.view.transform = CGAffineTransformMakeScale(0.0, 0.0);
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:YES];
        }];
    }
}
 
-(NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    return 0.4;
}

Any object conforming to the UIViewControllerAnimatedTransitioning protocol must implement animateTransition: and transitionDuration:. You can also implement the optional convenience method animationEnded:, which is invoked by the system when the animation is complete.

Inside animateTransition: you are responsible for the following:

  1. Insertion of the "to" view controller's view into the container view
  2. Placing the "to" and "from" view controllers views in their designated positions
  3. Invoking the context's completeTransition: method when you are finished (Don't forget this!)

The Animation Controller is passed a system object (transitionContext) conforming to UIViewControllerContextTransitioning that gives us the information we need to carry out the transition. From the context we get references to the following items:

Reference Code Description
Container View [transitionContext containerView] The container in which the transition takes place. For a modal presentation this is the view controller that is presenting the modal view. For a navigation controller transition this is a wrapper view that takes the size of the root view controller's view.
"From" View Controller
[transitionContext 
  viewControllerForKey:
  UITransitionContextFromViewControllerKey]
For a modal presentation this is the view controller that is doing the presenting. For a navigation controller transition this is the view controller that is currently displayed on the stack.
"To" View Controller
[transitionContext 
  viewControllerForKey:
  UITransitionContextToViewControllerKey]
For a modal presentation this is the view controller that is being presented. For a navigation controller transition this is the view controller that is being pushed onto the stack or popped to.
Initial Frame
[transitionContext 
  initialFrameForViewController:
  viewController]
The frame where each view controller's view begins the animation.
Final Frame
[transitionContext 
  finalFrameForViewController:
  viewController]
The frame where each view controller's view should be at the end of the animation.

Clearly Apple does most of the annoying work for free, leaving us to deal with the look and feel of the animation itself. A couple important notes about the context's role in the transition:

  • The frame calls may return CGRectZero if the system cannot determine an appropriate value. For example: asking for the final frame of a custom modal presentation.
  • The frame calls return CGRectZero if a view controller is being removed. For example: when calling finalFrameForViewController on the "from" view in a navigation transition.
  • You never have to worry about removing the "from" view controller's view from the hierarchy. The context handles that for you.
  • You can safely maintain a reference to the context within your animation controller if you need it elsewhere.

Plugging In Animation Controllers to Existing Transitions

Now that we've built our Animation Controllers there's one last step to plugging them into a built-in transition: we must vend them to the UIViewController that is managing the transition.

To do this in a regular UIViewController, we just conform to the new UIViewControllerTransitioningDelegate and provide the animationControllerForPresentedController and animationControllerForDismissedController methods. In my sample app I set a convenience property here to tell the animation controller whether we are presenting or dismissing:

-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    modalAnimationController.type = AnimationTypePresent;
    return modalAnimationController;
}
 
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    modalAnimationController.type = AnimationTypeDismiss;
    return modalAnimationController;
}

Then, when presenting our modal view controllers, we set our modalPresentationStyle to either UIModalPresentationFullScreen or the new UIModalPresentationCustom. We must also set its transitioningDelegate to our UIViewControllerTransitioningDelegate conforming object, most likely the UIViewController that's doing the presenting.

OptionsViewController *modal = [[OptionsViewController alloc] initWithNibName:@"OptionsViewController" bundle:[NSBundle mainBundle]];
modal.transitioningDelegate = self;
modal.modalPresentationStyle = UIModalPresentationCustom;
[self presentViewController:modal animated:YES completion:nil];

To plug into UINavigationController transitions we leverage the new animationControllerForOperation method that's built into the existing UINavigationControllerDelegate protocol. For any custom navigation transition the navigation bar will crossfade changes to its navigation items. Similarly, UITabBarController has animationControllerForTransitionFromViewController built into the existing UITabBarControllerDelegate protocol.

Interactive Transitions

Apple has made the interactive pop gesture pervasive throughout iOS 7, and they have given us a few tools to make our own transitions interactive as well. To do this, we follow the same pattern as above to return an Interaction Controller via the appropriate delegate method:

  • UINavigationController
    • interactionControllerForAnimationController
  • UITabBarController
    • interactionControllerForAnimationController
  • UIViewController
    • interactionControllerForPresentation
    • interactionControllerForDismissal

The only caveat here is that these methods don't fire if the transition is not a custom animation. For example, a UINavigationController will not call interactionControllerForAnimationController until a valid Animation Controller has been returned from animationControllerForOperation, whether or not you're actually using the animation controller for your interactive transition.

Also, interaction controllers are extremely flexible. For the sample app I am using gesture recognizers to control the interaction, however they need not be gesture driven. Anything that you can drive programmatically in an iterative way can be the source of an interactive transition.

Interaction Controllers: The Easy Way

There are two ways to create an interaction controller. The first (and easiest) is to use UIPercentDrivenInteractiveTransition.

@interface UIPercentDrivenInteractiveTransition : NSObject <UIViewControllerInteractiveTransitioning>
 
@property (readonly) CGFloat duration;
@property (readonly) CGFloat percentComplete;
@property (nonatomic,assign) CGFloat completionSpeed;
@property (nonatomic,assign) UIViewAnimationCurve completionCurve;
 
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
- (void)cancelInteractiveTransition;
- (void)finishInteractiveTransition;

This concrete class makes it easy to add interactivity to your existing Animation Controllers. Just have the target of your gesture recognizer (or whatever is driving your interaction) call out to updateInteractiveTransition: passing the percentage of the entire animation that is complete. Call finishInteractiveTransition: if the interaction was completed (i.e. the gesture passed your threshold of completion) and cancelInteractiveTransition: if the interaction did not complete. Here's an example of how it could work with a pinch gesture:

-(void)handlePinch:(UIPinchGestureRecognizer *)pinch {
    CGFloat scale = pinch.scale;
    switch (pinch.state) {
      case UIGestureRecognizerStateBegan: {
          _startScale = scale;
          self.interactive = YES;
          [self.navigationController popViewControllerAnimated:YES];
          break;
      }
      case UIGestureRecognizerStateChanged: {
          CGFloat percent = (1.0 - scale/_startScale);
          [self updateInteractiveTransition:(percent < 0.0) ? 0.0 : percent];
          break;
      }
      case UIGestureRecognizerStateEnded: {
          CGFloat percent = (1.0 - scale/_startScale);
          BOOL cancelled = ([pinch velocity] < 5.0 && percent <= 0.3);
          if (cancelled) [self cancelInteractiveTransition];
          else [self finishInteractiveTransition];
          break;
      }
      case UIGestureRecognizerStateCancelled: {
          CGFloat percent = (1.0 - scale/_startScale);
          BOOL cancelled = ([pinch velocity] < 5.0 && percent <= 0.3);
          if (cancelled) [self cancelInteractiveTransition];
          else [self finishInteractiveTransition];
          break;
      }
    }
}

When you subclass UIPercentDrivenInteractiveTransition Apple automatically uses your Animation Controller's animateTransition: to visually reflect the percentage of completion you pass to it. It also uses animateTransition: to transition back to a normal state once the interaction is complete, and you can change the completionSpeed and completionCurve properties to customize this behavior.

Interaction Controllers: The Custom Way

If you find yourself needing more control over how UIPercentDrivenInteractiveTransition handles transitions, scrap it and do it yourself using UIViewControllerInteractiveTransitioning. This protocol is much like UIViewControllerAnimatedTransitioning and allows you to have full control over how an interaction takes place. To use it you are responsible for the following:

  1. Implement startInteractiveTransition: to set up the transition
  2. Manage a reference to the transition's context object (UIPercentDrivenInteractiveTransition does this for you)
  3. Call out to the context's updateInteractiveTransition:, cancelInteractiveTransition and finishInteractiveTransition (For a navigation controller transition this is what updates the navigation bar crossfade)
  4. Call the context's transitionCompleted: when finished

Below are my custom interaction methods for the same scale animation I shared above, which allows the user to pop a view controller by pinching inwards.

-(void)startInteractiveTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    //Maintain reference to context
    _context = transitionContext;
 
    //Get references to view hierarchy
    UIView *containerView = [transitionContext containerView];
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
 
    //Insert 'to' view into hierarchy
    toViewController.view.frame = [transitionContext finalFrameForViewController:toViewController];
    [containerView insertSubview:toViewController.view belowSubview:fromViewController.view];
 
    //Save reference for view to be scaled
    _transitioningView = fromViewController.view;
}
 
-(void)updateWithPercent:(CGFloat)percent {
    CGFloat scale = fabsf(percent-1.0);
    _transitioningView.transform = CGAffineTransformMakeScale(scale, scale);
    [_context updateInteractiveTransition:percent];
}
 
-(void)end:(BOOL)cancelled {
    if (cancelled) {
        [UIView animateWithDuration:_completionSpeed animations:^{
            _transitioningView.transform = CGAffineTransformMakeScale(1.0, 1.0);
        } completion:^(BOOL finished) {
            [_context cancelInteractiveTransition];
            [_context completeTransition:NO];
        }];
    } else {
        [UIView animateWithDuration:_completionSpeed animations:^{
            _transitioningView.transform = CGAffineTransformMakeScale(0.0, 0.0);
        } completion:^(BOOL finished) {
            [_context finishInteractiveTransition];
            [_context completeTransition:YES];
        }];
    }
}

Feel free to have your Animation Controller also implement this protocol (like I did) to keep everything in one place. Or you can separate your Interaction Controllers from your Animation Controllers completely -- the beauty of the protocol-based approach is that it's up to you to create the best architecture for your needs.

Extra Goodies

Selectivity Inside an Animation Block

Ever had a big, beautiful list of changes within an animation block but need some of them to not be animated? Back before animation blocks we could call setAnimationsEnabled to selectively control what was animated using the [UIView beginAnimations] technique. In the iOS 7 SDK Apple has renewed this ability for animation blocks with the following:

[UIView performWithoutAnimation:^{
    //Guaranteed not to be animated
}];

Just call this inside any animation block and you'll be selectively animating in no time.

Collection View Navigation Transitions

You probably know all about the setLayout:animated: method of UICollectionView. In iOS 7 this method can be automatically called when pushing and popping between UICollectionViewControllers in a navigation stack, using the useLayoutToLayoutNavigationTransitions property. Just set this property on a collection view controller you are pushing onto the stack and your navigation controller will automatically animate the transition as if you called setLayout:animated: on the visible collection view.

CollectionViewController *VC = [[CollectionViewController alloc] initWithCollectionViewLayout:flowLayout];
VC.title = @"Mini Apples";
VC.useLayoutToLayoutNavigationTransitions = YES;
[self.navigationController pushViewController:VC animated:YES];

Transition Coordinators

Another helpful API to assist with UIViewController transition management is the UIViewControllerTransitionCoordinator protocol. In iOS 7 every UIViewController (which of course includes UINavigationController and UITabBarController) has a transitionCoordinator property which provides a bunch of great tools to use during a transition. Check out animateAlongsideTransition:

[self.transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    //Animations
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    //Completion Block
}];

This method allows you to run any animation parallel to a UIViewController-managed transition even if it's not in the same view hierarchy. The context parameter passed into the animation block is just like the UIViewControllerContextTransitioning protocol we talked about before, giving you information such as the transition's container view and modal presentation style (if applicable). Apple even notes in the interface that "It is perfectly legitimate to only specify a completion block," so if that's all you need it for, go right ahead.

For interactive transitions, the notifyWhenInteractionEndsUsingBlock: method is particularly useful for managing view states that may have changed during a transition. When using interactive transitions the viewWillAppear: method may be called on participating view controllers during the transition, but you can't depend on viewDidAppear: to be called afterwards since interactive transitions can be cancelled. By using this method you can reverse any changes that you don't want in those situations (using the isCancelled property of UIViewControllerTransitionCoordinatorContext).

[self.transitionCoordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    //Completion Block
}];

Snapshots

Up until now making a visual copy of a UIView involved creating a UIGraphics image context, rendering its layer in said context, getting an image from the rendered context, closing the context and then loading the image into a UIImageView. Now we have this single line:

[view snapshotViewAfterScreenUpdates:NO];

This essentially makes a flattened copy of an entire UIView, which is really useful for complex animations where we don't want our views laying out their subviews throughout the animation. This method won't replace UIGraphics for grabbing a UIImage out of a view, or for grabbing specific sections of views.

The screen updates flag lets us decide if the snapshot should wait until all updates have been applied to the view or just grab it in its current state. Therefore, the following would be a blank snapshot:

[view snapshotViewAfterScreenUpdates:YES];
[view setAlpha:0.0];

As long as you pass YES the snapshot will appear to be empty since it is including the change in alpha to the view it is snapshotting.

And yes, snapshots of snapshots (of snapshots...) are supported.

Conclusion

With iOS 7 Apple continues to refine and expand tools for creating and maintaining animations. Not only has the iOS 7 SDK introduced great new animation block and convenience methods, it has also established an entirely new paradigm for organizing and manipulating transitions throughout the view controller ecosystem.

Don't forget to check out the sample app below for a working example of everything I've covered in this article!

Attachments

VCTransitions.zip