View Transition Animations - Bleeding Edge Press Developing an iOS 7 Edge (2013)

Bleeding Edge Press Developing an iOS 7 Edge (2013)

3. View Transition Animations

IN THIS CHAPTER

· Which transitions can be customized

· Anatomy of a custom transition

· Customizing a transitions

· Making a custom transition interactive

· Canceling and coordinating transitions

In iOS 6 and before, if you wanted to animate the transition from one view controller to another, you either used pushViewController:animated: and popViewControllerAnimated: for transitions inside navigation controllers or presentViewController:animated:completion: and dismissViewControllerAnimated:completion: for modally presenting and dismissing view controller. Now with iOS 7, Apple introduces a powerful new set of protocols that allow you to go beyond animated:YES. In this chapter we will walk through the details of the new custom transition protocol.

Which Transitions Can Be Customized?

With iOS 7, Apple enables you to customize a lot of the view controller transitions that your users will see. Before we delve into the details of how this new technology works, let us see which transitions can be customized:

Modal Presentations and Dismissals

Modal transitions can be customized as long as the presented view controller has either the UIModalPresentationFullScreen or UIModalPresentationCustom as its modalPresentationStyle.

Tab Bar Controller Selection Changes

Prior to iOS 7, UITabBarController did not provide any animated transitions. Now with iOS 7 setSelectedViewController: or setSelectedIndex: calls are automatically animated, as long as the delegate can vend an object which animates the transition and implements the UIViewControllerAnimatedTransitioning protocol.

Navigation Controller Pushes and Pops

Navigation stack changes have been animated with the well-known slide animation since the first iPhone. Now with iOS 7, you can replace the slide animation when pushing and popping view controllers by vending an object responsible for the animation from your UINavigationControllerDelegate.

Additionally, if you are transitioning from one UICollectionViewController instance to another inside the navigation controller, you can opt to animate the layout change inside the same UICollectionView. Apple does this with its own Photos.app by setting useLayoutToLayoutNavigationTransitions=YES for your collection view controllers.

Anatomy of a Custom Transition

The API for the custom transitions is very powerful and flexible, but at the same time there are numerous classes and protocols which interact with each other to successfully perform a custom transition. Therefore, it is helpful to get a sense of what's happening behind the scenes before we proceed to code examples.

A view controller transition is an inconsistent UI state because of its very nature: during the transition animation there are two sibling view controllers whose views are visible on the screen without containing each other. It has been possible to hack custom transitions between view controllers before iOS 7, but they relied on the knowledge about the internals of the UIKit and were very easy to get wrong and end up in an inconsistent state. Not to mention, those hacks could break with changes to UIKit internals. iOS 7 custom transitions are managed by a set of protocols that encapsulate the complexity behind the scenes and give you a safe "sandbox" environment to perform your animations.

An outline of a custom transition looks like this:

1. The transition gets triggered either through user interaction or programmatically.

2. The system asks current view controller's transitioning delegate for an animation controller to manage the animation.

3. If the delegate can vend such an object, the custom transition will be performed. Otherwise the system falls back to default animation.

4. If the delegate vends an animation controller, the system asks for an interaction controller object as well. If an interaction controller is vended by the delegate the transition will be performed interactively.

5. viewWillAppear: is sent to the view controller which is going to appear. If the transition is interactive the appearing view should ask the transition coordinator here to be notified when the transition finishes or gets cancelled.

6. The animation controller performs the required animations inside the transitioning context provided by the system. This can either happen interactively or not depending upon the existence of an interaction controller.

7. Once the animations are either completed (or cancelled), the animation controller MUST report this to the transitioning context so that the system can clean up the transitioning context and maintain internal state.

As you can gather from the above steps, there are a lot of delegates sending messages to each other during the transition. Let's introduce them:

Animation Controllers are objects that conform to the UIViewControllerAnimatedTransitioning protocol. Their single purpose is to perform the animation.

Interaction Controllers are objects that conform to the UIViewControllerInteractiveTransitioning protocol. They are responsible for controlling the animation controller depending upon the interactive input, which could be either a user gesture or some other programmatically computed value. Apple provides the UIPercentDrivenInteractiveTransition class, which implements the UIViewControllerInteractiveTransitioning protocol and can drive any animation controller as long as its animations are implemented using UIView block-based animation.

Transitioning Delegates are responsible for vending the correct animation and interaction controllers depending on the nature of the transition to be performed. Depending upon the type of the parent view controller they conform to UIViewControllerTransitioningDelegate, UINavigationControllerDelegate or UITabBarControllerDelegate.

Transitioning Contexts are objects conforming to the UIViewControllerContextTransitioning protocol. They define the "sandbox" environment in which the transition is taking place. They are initialized by the system and passed on to the animation controllers and interaction controllers.

Transition Coordinators are objects which conform to UIViewControllerTransitionCoordinator. They are responsible for running the registered blocks when transitions finish or get cancelled so that view controllers can clean up and undo changes when a transition gets cancelled. They also provide methods to run other animations in parallel to the transition animations. Using those methods you can also run your custom animations alongside standard system transitions.

Even though there are numerous steps to be coordinated and many actors to be managed, don't let yourself be intimidated by the complexity. Let's look at some example code from our BepBop app in the next sections and you'll be writing your own custom transitions in no time!

Customizing Transitions

In this section we will take a look at some code from the BepBop app in order to walk through some examples of custom transitions. The complete source code for the BepBop app can be found on github: https://github.com/iosedgeapp/iOSEdge.

Tab Change

In the BepBop app, we have implemented a sliding tab change animation, where selecting a tab would cause the view for the current tab to horizontally slide out of screen and the view for the selected tab to slide in with a small black gap between them.

Sliding tab transition

In order to animate the change of the selected tab in a tab bar controller, we need to assign a delegate to our tab bar controller, which can vend an animation controller. Since our tab bar controller does nothing except to show the two other custom transition demos, we decided to keep it simple and unify minimal custom transitioning fuctionality in a single UITabBarController subclass, which implements both UITabBarControllerDelegateand UIViewControllerAnimatedTransitioning. That's why we can get away with assigning self as the delegate during initialization:

0001: self.delegate = self;
0002:
0003: UIViewController* root = [[BEPNavigationTransitionsRootViewController alloc] initWithNibName:nil bundle:nil];
0004:
0005: NSArray* vcs = @[
0006: [[BEPPresentationTransitionsViewController alloc] initWithNibName:nil bundle:nil],
0007: [[BEPNavigationTransitionsViewController alloc] initWithRootViewController:root]
0008: ];
0009:
0010: [self setViewControllers:vcs
0011: animated:NO];
0012: self.selectedIndex = 0;

For custom transition animations to work, our tab bar controller delegate needs to vend an animation controller when asked. Since our tab bar controller itself implements the UIViewControllerAnimatedTransitioning protocol, once again we can get away with returning self:

0001:- (id ) tabBarController:(UITabBarController*)tabBarController
0002: animationControllerForTransitionFromViewController:(UIViewController*)fromVC
0003: toViewController:(UIViewController*)toVC
0004:{
0005: return self;
0006:}
0007:

Now that the system has an animation controller instance, it will ask for the duration of the animation once the interaction transition starts. We return a constant duration for each tab transition:

0001:- (NSTimeInterval) transitionDuration:(id)transitionContext
0002:{
0003: return 0.5;
0004:}

Once the system has gathered all the necessary information, set up the context and it is ready to start the transition. Our animator object will be asked to perform the animation:

0001:- (void) animateTransition:(id)transitionContext
0002:{
0003: static const CGFloat DampingConstant = 0.75;
0004: static const CGFloat InitialVelocity = 0.5;
0005: static const CGFloat PaddingBetweenViews = 20;
0006:
0007: UIView* inView = [transitionContext containerView];
0008: UIViewController* fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
0009: UIView* fromView = [fromVC view];
0010: UIViewController* toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
0011: UIView* toView = [toVC view];
0012:
0013: inView.backgroundColor = [UIColor blackColor];
0014:
0015: CGRect centerRect = [transitionContext finalFrameForViewController:toVC];
0016: CGRect leftRect = CGRectOffset(centerRect, -(CGRectGetWidth(centerRect)+PaddingBetweenViews), 0);
0017: CGRect rightRect = CGRectOffset(centerRect, CGRectGetWidth(centerRect)+PaddingBetweenViews, 0);
0018:
0019:
0020: if (fromVC == self.viewControllers[0]) // we are transitioning from the first VC to the second
0021: {
0022: toView.frame = rightRect;
0023:
0024: [inView addSubview:toView];
0025:
0026: [UIView animateWithDuration:[self transitionDuration:transitionContext]
0027: delay:0.0
0028: usingSpringWithDamping:DampingConstant
0029: initialSpringVelocity:InitialVelocity
0030: options:0
0031: animations:^{
0032: fromView.frame = leftRect;
0033: toView.frame = centerRect;
0034: }
0035: completion:^(BOOL finished) {
0036: [transitionContext completeTransition:YES];
0037: }];
0038: }
0039: else if (fromVC == self.viewControllers[1]) // we are transitioning from the second VC to the first
0040: {
0041: toView.frame = leftRect;
0042: [inView addSubview:toView];
0043: [UIView animateWithDuration:[self transitionDuration:transitionContext]
0044: delay:0.0
0045: usingSpringWithDamping:DampingConstant
0046: initialSpringVelocity:-InitialVelocity
0047: options:0
0048: animations:^{
0049: fromView.frame = rightRect;
0050: toView.frame = centerRect;
0051: }
0052: completion:^(BOOL finished) {
0053: [transitionContext completeTransition:YES];
0054: }];
0055: }
0056:}

Even though the code above looks quite big, most of it is relatively uninteresting. Let's walk through it and concentrate on the important points. The transitionContext provides us the necessary view controllers and views to provide our animation with. Its containerView property returns us the container where all of the animation will take place:

UIView* inView = [transitionContext containerView];

This view is a temporary sandbox view outside the normal view hierarchy of our app. The disappearing and appearing view controllers are also in the transition context and are obtained with the following lines:

0001: UIViewController* fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
0002: UIView* fromView = [fromVC view];
0003: UIViewController* toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
0004: UIView* toView = [toVC view];

The next step is to figure out which direction we are animating. This is achieved by comparing the fromVC with the tab bar controller's view controllers:

0001: if (fromVC == self.viewControllers[0]) // we are transitioning from the first VC to the second
0002: {
0003: ...
0004: }
0005: else if (fromVC == self.viewControllers[1]) // we are transitioning from the second VC to the first
0006: {
0007: ...
0008: }

Once we know which direction we are animating, performing the animation is quite straight forward using the UIView block-based animations:

0001: toView.frame = rightRect;
0002:
0003: [inView addSubview:toView];
0004:
0005: [UIView animateWithDuration:[self transitionDuration:transitionContext]
0006: delay:0.0
0007: usingSpringWithDamping:DampingConstant
0008: initialSpringVelocity:InitialVelocity
0009: options:0
0010: animations:^{
0011: fromView.frame = leftRect;
0012: toView.frame = centerRect;
0013: }
0014: completion:^(BOOL finished) {
0015: [transitionContext completeTransition:YES];
0016: }];

Here, it is important to pay attention to two very critical points:

1. It is the animation controllers explicit responsibility to insert toView into inView's view hierarchy. Here we used addSubview: but depending on the situation it might be necessary to call insertSubview:aboveSubview:and insertSubview:belowSubview:.

2. Once the animations are completed the animation MUST call completeTransition: on the transitionContext.

Also, please note how we used the new animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion: API, introduced in iOS 7, to give our sliding animation a little bit of springiness without diving deep into UIKit Dynamics.

Modal Presentation

Now that we see how the simplest case of custom transitions works, let's continue with a slightly more complex case where we will perform different animations during the presentation and dismissal of a modal view controller. In the BepBop app we implemented a rotating and falling from the top modal view presentation and a falling down modal view dismissal.

Modal Presentation

For a custom modal animations to work, we first need to set a transition delegate to the presented view controller:

0001: BEPSimpleImageViewController* ivc = [[BEPSimpleImageViewController alloc] init];
0002: ivc.image = [UIImage imageNamed:@"Canyon.jpg"];
0003: ivc.modalPresentationStyle = UIModalPresentationCustom;
0004: ivc.transitioningDelegate = self;
0005: [self presentViewController:ivc animated:YES completion:nil];

Once again we are being lazy and declare the current view controller to implement the UIViewControllerTransitioningDelegate protocol. While it's perfectly OK to do so if you know you're not going to reuse custom transitions, we recommend refactoring this delegate functionalities to different classes if you're planning to reuse your custom transitions at multiple places.

Before the presentation transition starts, the system is going to ask for an animation controller using the delegate method:

0001:- (id) animationControllerForPresentedController:(UIViewController*)presented
0002: presentingController:(UIViewController*)presenting
0003: sourceController:(UIViewController*)source
0004:{
0005: return [[BEPModalTransitionAnimator alloc] initWithDirection:BEPModelTransitionDirectionPresent];
0006:}

If the system were dismissing the modally presented view controller, it would have called the following method:

0001:- (id) animationControllerForDismissedController:(UIViewController*)dismissed
0002:{
0003: return [[BEPModalTransitionAnimator alloc] initWithDirection:BEPModelTransitionDirectionDismiss];
0004:}

Please note how we initialize our animation controller differently depending on the modal operation. Inside the BEPModalTransitionAnimator implementation the basic logic is very similar to how we implemented the tab change animation above, so we won't be discussing it in great detail but just point out some important differences. First thing to note is, we explicitly insert the presented view above the current view:

[inView insertSubview:toView aboveSubview:fromView];

And the second important point is that we can set custom initial and final frames for the toView and fromView:

0001: CGRect finalRect = CGRectInset(fromView.frame,
0002: CGRectGetWidth(fromView.frame)/4,
0003: CGRectGetHeight(fromView.frame)/4);
0004: CGRect initialRect = CGRectOffset(finalRect, 0, -500);

And finally, during a dismissal transition we don't need to insert toView into the view hiearchy because during a modal presentation the presenting view controller's view is not removed form the view hieararchy.

Navigation Push and Pop

As far as the complexity of the transition is considered, the navigation push transition we used in the BepBop app is very similar to the modal presentation transition we just discussed. Therefore, we leave it as an exercise for you to look at the source code and compare it to the modal presentation.

The navigation pop animation controller BEPNavigationTransitionsPopAnimator, on the other hand, is where things start to get interesting. You will notice that the pop animation controller is not just responsible for performing the animation but also doubles as an interaction controller in order to support an interactive pinch-to-pop gesture, which we will discuss in more detail in next section. The second interesting thing about the pop animation controller is the pop animation itself, which looks like the photo is folding itself in four before disappearing from view. Let's quickly go over the code to see how you can achieve such an effect in your own apps.

Folding navigation pop animation

First thing we do is to take four snapshots, one for each quadrant of the view that is being popped, using the new snapshot API:

UIView* topLeft = [fromView resizableSnapshotViewFromRect:CGRectMake(0, 0, foldWidth, foldHeight)0001: afterScreenUpdates:NO
0002: withCapInsets:UIEdgeInsetsZero];

The iOS 7 snapshot methods snapshotViewAfterScreenUpdates: and resizableSnapshotViewFromRect:afterScreenUpdates:withCapInsets: are about an order of magnitude faster than the old renderInContext: API. Once we have the four snapshot views, we fold them twice using the block-based keyframe animations API, also new in iOS 7:

0001:[UIView animateKeyframesWithDuration:[self transitionDuration:transitionContext]
0002: delay:0.0
0003: options:0
0004: animations:^{
0005: [UIView addKeyframeWithRelativeStartTime:0.0
0006: relativeDuration:0.4
0007: animations:^{
0008: CATransform3D firstFolding = CATransform3DMakeRotation(M_PI, 0, 1, 0);
0009: topRight.layer.transform = firstFolding;
0010: bottomRight.layer.transform = firstFolding;
0011: topRight.layer.zPosition = 1;
0012: bottomRight.layer.zPosition = 1;
0013: }];

The first folding, 180 degrees along the y-axis, gets only applied to the both snapshot views on the right, resulting in a smooth folding of the right half of the photo on top of the left half. The second folding on the other hand is along the x-axis and folds the bottom two snapshots on top of the top two:

0001:[UIView addKeyframeWithRelativeStartTime:0.4
0002: relativeDuration:0.4
0003: animations:^{
0004: CATransform3D secondFolding = CATransform3DMakeRotation(M_PI, 1, 0, 0);
0005: bottomRight.layer.transform = CATransform3DConcat(bottomRight.layer.transform, secondFolding);
0006: bottomRight.layer.zPosition = 2;
0007: bottomLeft.layer.transform = secondFolding;
0008: bottomLeft.layer.zPosition = 3;
0009: }];

And as the final keyframe step we let all four snapshots disappear:

0001: [UIView addKeyframeWithRelativeStartTime:0.8
0002: relativeDuration:0.2
0003: animations:^{
0004: topLeft.alpha = 0;
0005: topRight.alpha = 0;
0006: bottomLeft.alpha = 0;
0007: bottomRight.alpha = 0;
0008: }];
0009: }

Now that we have completed the animation, the completion handler is slightly more involved because this transition can be interactive and we have to handle the case where the transition is cancelled, which brings us nicely to the next section.

Making a Custom Transition Interactive

If you have a transitioning delegate which vends non-nil animation controllers, the system will always ask it for a corresponding interaction controller using tabBarController:interactionControllerForAnimationController:, navigationController:interactionControllerForAnimationController:, interactionControllerForPresentation: or interactionControllerForDismissal: depending on the situation. You can always return nil if you (or not implement the corresponding method) if you want the transition to be non-interactive. If you return an object conforming to UIViewControllerInteractiveTransitioning your transition will be interactive. Let's have a look at how we implemented the BEPNavigationTransitionsPopAnimator to see how an interactive transition can be managed.

First of all, BEPNavigationTransitionsPopAnimator inherits from UIPercentDrivenInteractiveTransition such that the bulk of the workload of interpolating between different animation states is handled by the system. Secondly, our interaction controller is initialized with the corresponding navigation controller and adds a pinch gesture recognizer to its navigation controller during init:

0001:- (instancetype) initWithNavigationController:(UINavigationController*)nc
0002:{
0003: if (self = [super init])
0004: {
0005: self.parent = nc;
0006: UIPinchGestureRecognizer* pgr = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
0007: [self.parent.view addGestureRecognizer:pgr];
0008: }
0009: return self;
0010:}

This pinch gesture recognizer is responsible for starting the transition as well as driving it once it has started:

0001:- (void) handlePinch:(UIPinchGestureRecognizer*)gr
0002:{
0003: CGFloat scale = [gr scale];
0004:
0005: switch ([gr state])
0006: {
0007: case UIGestureRecognizerStateBegan:
0008: self.interactive = YES; _startScale = scale;
0009: [self.parent popViewControllerAnimated:YES];
0010: break;
0011: case UIGestureRecognizerStateChanged: {
0012: CGFloat percent = (1.0 - scale/_startScale);
0013: [self updateInteractiveTransition:MAX(percent,0.0)];
0014: break;
0015: }
0016: case UIGestureRecognizerStateEnded:
0017: case UIGestureRecognizerStateCancelled:
0018: if ([gr velocity] >= 0.0 || [gr state] == UIGestureRecognizerStateCancelled)
0019: {
0020: [self cancelInteractiveTransition];
0021: }
0022: else
0023: {
0024: [self finishInteractiveTransition];
0025: }
0026: self.interactive = NO;
0027: break;
0028: default:
0029: break;
0030: }
0031:}

Once the pinch gesture is recognized, the navigation controller is told to pop the current view controller with animation, which in turn kicks off the folding animation we have implemented in the previous section. However since the animation is managed by an interaction controller, the pop transition does not run automatically but is constantly updated every time the user moves their pinch gesture. Once the user finishes the pinch gesture, the transition is either canceled or finished depending on which way the user was pinching last.

Cancelling and Coordinating Transitions

If you are using interactive custom transitions you have to pay attention to how your view controllers handle the cancellation of the transition. If you consider the life cycle of a view controller, the assumption has always been that the life-cycle methods get called in a certain order:

1. viewWillAppear:

2. viewDidAppear:

3. viewWillDisappear:

4. viewDidDisappear:

Now with interactive custom transitions, this assumed order can be broken if the transition gets cancelled. The appearing view controller is sent viewWillAppear: when the transition starts, but if the transition is cancelled viewWillDisappear: will be sent after viewWillAppear: instead of viewDidAppear: such that the final order becomes:

1. viewWillAppear:

2. viewWillDisappear:

3. viewDidDisappear:

If your view controller is doing some setup in viewWillAppear: with the assumption that viewDidAppear: will follow, you might need to do some clean up if the transition gets cancelled in order to not leak memory and leave everything in a predictable state. This is where the transition coordinator comes in very handy. The transition coordinator is an object conforming to UIViewControllerTransitionCoordinator and is vended by container view controllers during a transition. Here is an example of how it might be used in viewWillAppear::

0001:- (void) viewWillAppear: {
0002: [self doSomeSideEffectsAssumingViewDidAppearIsGoingToBeCalled];
0003: id <UIViewControllerTransitionCoordinator> coordinator;
0004: coordinator = [self transitionCoordinator];
0005: if(coordinator && [coordinator initiallyInteractive]) {
0006: [transitionCoordinator notifyWhenInteractionEndsUsingBlock:^(id <UIViewControllerTransitionCoordinatorContext> ctx) {
0007: if(ctx.isCancelled) {
0008: [self undoSideEffects];
0009: }
0010: }];
0011: }
0012:}

The transition coordinator is also very useful if you'd like to keep the default system animations but would like to perform some other animations in your views during the transition. In that case you can give the transition coordinator a block of animations which should be run alongside the default system animations such that the start time and the durations match exactly:

0001:UIViewController *vc;
0002:[self pushViewController:vc animated: YES];
0003:id <UIViewControllerTransitionCoordinator> coordinator coordinator = [viewController transitionCoordinator];
0004:[coordinator animateAlongsideTransition:
0005: ^(id<UIViewControllerTransitionCoordinatorContext> context) {
0006: // some animation
0007: }
0008: completion:(id<UIViewControllerTransitionCoordinatorContext> context) {
0009: // Code to run after your push transition has finished.
0010:}];

In the code above both parameters are optional and can be nil.

Summary

In this chapter, we covered how the new custom transitions work. We first discussed how a custom transition works and how various objects interact with each other to make sure the transition starts, runs and completes with no errors and results in a consistent view controller and view hierarchy. Afterwards we looked at a couple of examples of how custom transitions can be implemented in different container view controllers. Finally, we concluded by going through an example of an interactive custom transition and by showing how transitions can be correctly cancelled and coordinated with other animations.