Custom View Transitions on iOS 7

April 02, 2014

iOS 7 introduced a new way to build custom transitions between views and it’s now fairly easy to create complex animations for navigation or segue events. The built-in containment view controllers have been outfitted for use with transition APIs, including UICollectionViewController which sports a new useLayoutToLayoutNavigationTransitions property. This property allows two UICollectionViews to animate between layouts within a navigation stack. There is sample code from Apple and Stack Overflow posts detailing how use this property.

I wanted to utilize the new transition APIs to build a slide-and-zoom animation, where one collection view cell slides out and zooms to a new position in the subsequent collection view. While the built-in transitions would have worked, I wanted to dig a little deeper and create a custom view transition. There are several great articles about this, but two that I found especially helpful are by Chris Eidhof and Bradford Dillon. Update: The May 2014 issue of objc.io is dedicated to Animations and includes some fantastic articles.

The transition I created ended up looking like this:

// FIXME

In the implemenation there are two UIViewControllers contained within a UINavigationController. When a cell in the first view controller is tapped, the second view controller is pushed onto the navigation stack. Standard stuff. The animation from one view controller to another is handled by a UINavigationControllerDelegate instance that implements the navigationController:animationControllerForOperation:fromViewController:toViewController delegate method. This method returns an animator object depending upon which view controller will be shown.

// initalize the collection view controller with a layout
PASListCollectionViewLayout *layout = [[PASListCollectionViewLayout alloc] init];
PASViewController *viewController = [[PASViewController alloc] initWithCollectionViewLayout:layout];

// set the delegate of the navigation controller so we can
// create custom animator objects for transitioning between view controllers
self.navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
[self.navigationController setDelegate:self];

In the UINavigationControllerDelegate method, determine which view controllers are being navigated between and return an object to handle the animation. In my case, I used different objects to handle the show and hide transitions, but this may not be necessary depending on the complexity of the animation. I found it convenient to refer to these as animator objects and make them subclasses of NSObject.

- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                  animationControllerForOperation:(UINavigationControllerOperation)operation
                                               fromViewController:(UIViewController *)fromVC
                                                 toViewController:(UIViewController *)toVC {
    if ([toVC isKindOfClass:[PASDetailViewController class]] && [fromVC isKindOfClass:[PASViewController class]]) {
        return [[PASShowDetailAnimator alloc] init];
    }
    else if ([toVC isKindOfClass:[PASViewController class]] && [fromVC isKindOfClass:[PASDetailViewController class]]) {
        return [[PASHideDetailAnimator alloc] init];
    }
    return nil;
}

The objects returned must conform to the UIViewControllerAnimatedTransitioning protocol, with the actual animation handled by animateTransition:. Inside animateTransition:, you’ll be given an instance of an object conforming to UIViewControllerContextTransitioning, refered to as the transition context. You can query the transition context for animatable views and a container view to carry out the animation. A simple animation might involve fading out the origin view controller while fading in the destination view controller.

There are resources available that describe how the transition context and animation work, but essentially a container view is inserted between the two animating views and the animation is performed therein. The origin and destination view controllers are retrived from the transition context. The destination view controller is added to the container view and faded into view. Conversely, the origin view controller is faded out of view and removed from the view hierarchy when the animation ends. For a more complex transitions UIView provides a resizableSnapshotViewFromRect:afterScreenUpdates:withCapInsets: method for creating lightweight resizable images from rectangular regions on the screen. This allows for different images of the view hierarchy to be created, added to the containter view and animated individually.

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UICollectionView *fromCollectionView = [(UICollectionViewController *)fromViewController collectionView];
    
    UIView *containerView = [transitionContext containerView];
    [containerView setBackgroundColor:self.backgroundColor];
    
    [toViewController.view setAlpha:0.0f];
    [containerView addSubview:toViewController.view];
    
    CGRect originRect = [self originRectFromContext:transitionContext];
    CGRect destinationRect = [self destinationRectFromContext:transitionContext];
    
    CGRect firstRect = CGRectMake(destinationRect.origin.x, destinationRect.origin.y, destinationRect.size.width, originRect.size.height);
    CGRect secondRect = CGRectMake(destinationRect.origin.x, destinationRect.origin.y, destinationRect.size.width, destinationRect.size.height);
    
    UIEdgeInsets insets = [self defaultInsets];
    UIView *snapshot = [fromCollectionView resizableSnapshotViewFromRect:originRect afterScreenUpdates:NO withCapInsets:insets];
    [snapshot setFrame:[containerView convertRect:originRect fromView:fromCollectionView]];
    [containerView addSubview:snapshot];
    
    [UIView animateKeyframesWithDuration:PASPassAnimationDuration delay:0.0f options:0 animations:^{
        [UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:0.33f animations:^{
            [fromViewController.view setAlpha:0.0f];
        }];
        [UIView addKeyframeWithRelativeStartTime:0.33f relativeDuration:0.33f animations:^{
            [snapshot setFrame:firstRect];
        }];
        [UIView addKeyframeWithRelativeStartTime:0.66f relativeDuration:0.33f animations:^{
            [snapshot setFrame:secondRect];
        }];
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:finished];
        [toViewController.view setAlpha:1.0f];
        [fromViewController.view removeFromSuperview];
        [containerView addSubview:toViewController.view];
        [snapshot removeFromSuperview];
    }];
}

The complete implementation of the slide-and-zoom animator is available as a gist.