iOS View Controller Transitions (Part 1)

Software

iOS has always had a heavy emphasis on animations, and for good reason. Animations can not only add some extra aesthetic appeal, they facilitate a functional understanding of an interface. Animations give users useful visual cues and context. In iOS, one of the most important applications of this concept is transitioning between ‘scenes’ of an application, or in programmer speak, switching from one UIViewController to another. In this post I will explain how to accomplish custom transitions between iOS scenes using Interface Builder and objective C.

Step 1: The Simplest Set-Up

In order to demonstrate these transitions, I’m going to attempt to make the simplest possible functioning example:

transitionStoryboard

The first step is to set up a navigation controller (scene A) and two scenes to transition between. Our first scene (scene B) has a button that will trigger a transition to the next scene (scene C), which has a different background color so that it will be visually distinguishable. Scene B has the following code:


– (void)viewDidLoad
{
[super viewDidLoad];
self.navigationController.delegate = self;
}

Any transition between scenes will be arbitrated by a special type of controller which handles navigation (scene A in our example). This special controller is usually a UINavigationController, but you can also use a UITabBarController. The navigation controller has a delegate object that it queries to determine if it needs a custom transition. You can see in our viewDidLoad function we set our controller for scene B as the delegate for scene A’s controller.

As the delegate, our scene B controller needs to implement the UINavigationControllerDelegate protocol:


#pragma mark – UINavigationControllerDelegate
– (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
return [[TTFadeAnimator alloc] init];
}

The protocol requires the delegate to return an animator object, which will inform the navigation controller on how to perform the animation. In this example, our object is the TTFadeAnimator. We will need to custom write this object for each unique type of transition that we want. Additionally, as we can see from the return type of the function above, the animator object must implement the UIViewControllerAnimatedTransitioning protocol.

The TTFadeAnimator in this example is a simple crossfade. The following code is the entire implementation:


#define kTransitionDuration 0.35
// …
#pragma mark – UIViewControllerAnimatedTransitioning
– (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return kTransitionDuration;
}
– (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
//fade the new view in
UIViewController* toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView* container = [transitionContext containerView];
toController.view.alpha = 0.0f;
[container addSubview:toController.view];
[UIView animateWithDuration:kTransitionDuration animations:^{
toController.view.alpha = 1.0f;
} completion:^(BOOL finished){
[fromController.view removeFromSuperview];
[transitionContext completeTransition:finished];
}];
}

Almost every line here is boilerplate. The two exceptions are the lines that set the view’s alpha to 0.0f, and then back to 1.0f. Let’s examine the rest of the code to see what we can learn about the way the animator is expected to function.

For reasons I will explain in the following post, the best practice here is to use only one animation block in this function. In this simplified case, we would be able to get away with having multiple animation blocks. However, we will see in a later example why multiple blocks would cause erroneous behavior in more complex transitions.

One of the more unexpected duties of the animator object is that it must manage the view hierarchy of both scene B and scene C. Our animateTransition: function is passed a UIViewControllerContextTransitioning argument that has an accessor for a container view. We are granted the freedom to do whatever we want within this container to achieve the desired transition effect, which is very powerful.

On the flip side, this container view mechanism can be error prone. Even though our navigation controller will push scene C onto the nav stack, if we neglect to add scene C’s view, it will not appear. In the above example you can see the line of code that adds the view just before the animation block. For similar reasons, you can see in the animation completion block that it is necessary to remove the old view when the transition is over.

Lastly, we need to always remember to call the completeTransition: function on the context object. This call officially ends the transition, and without it, the app will hang at the end of the animation.

So far, so good. Let’s take a look at our transition:

transition1

Testing the crossfade in the simulator


The code for this section is available in the ‘Step1_crossfade’ directory here.


Step 2: Using Snapshots

The previous section should be all you need to make some basic transitions. But we shouldn’t be satisfied with only the most basic features. One of the more frequently requested transitions is to have part of the screen open up to reveal more detail about a specific part of the UI. In the next example, we’ll look at what it would take to accomplish this.

The iOS snapshot API provides us with a very efficient way to capture the content of a UIView and use that content in an animation. We will need this capability to create the illusion of a view that splits open to reveal the next view inside it.

Because we want a new type of transition, we will need some new animator objects. Here is a snippet of updated code from scene B’s controller:


#pragma mark – UINavigationControllerDelegate
– (id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC
{
switch(operation)
{
case UINavigationControllerOperationPush:
return [[TTPushAnimator alloc] init];
case UINavigationControllerOperationPop:
return [[TTPopAnimator alloc] init];
default:
return nil;
}
}

Here we have replaced our fade animator with two new ones: TTPushAnimator and TTPopAnimator. The delegate pattern used by the navigation controller allows our scene B controller to use any arbitrary logic we want to determine what animation should be used. In this example, we use the operation argument to detect if the transition is a push or pop operation and supply the corresponding type of animator. As you might suspect, both animators do the same type of animation, but in different ‘directions’.

One other interesting thing to note is that if this function returns nil, the navigation controller will use the default iOS push animation for the transition.

Next let’s take a look at where the meat of this example happens, the implementation of the TTPushAnimator class:


– (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
UIViewController* toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIView* container = [transitionContext containerView];
//get rects that represent the top and bottom halves of the screen
CGSize viewSize = fromController.view.bounds.size;
CGRect topFrame = CGRectMake(0, 0, viewSize.width, viewSize.height/2);
CGRect bottomFrame = CGRectMake(0, viewSize.height/2, viewSize.width, viewSize.height/2);
//create snapshots
UIView* snapshotTop = [fromController.view resizableSnapshotViewFromRect:topFrame afterScreenUpdates:NO withCapInsets:UIEdgeInsetsZero];
UIView* snapshotBottom = [fromController.view resizableSnapshotViewFromRect:bottomFrame afterScreenUpdates:NO withCapInsets:UIEdgeInsetsZero];
snapshotTop.frame = topFrame;
snapshotBottom.frame = bottomFrame;
//remove the original view from the container
[fromController.view removeFromSuperview];
//add our destination view
[container addSubview:toController.view];
//add our snapshots on top
[container addSubview:snapshotTop];
[container addSubview:snapshotBottom];
[UIView animateWithDuration:kTransitionDuration animations:^{
//adjust the new frames
CGRect newTopFrame = topFrame;
CGRect newBottomFrame = bottomFrame;
newTopFrame.origin.y -= topFrame.size.height;
newBottomFrame.origin.y += bottomFrame.size.height;
//set the frames to animate them
snapshotTop.frame = newTopFrame;
snapshotBottom.frame = newBottomFrame;
} completion:^(BOOL finished){
//don't forget to clean up
[snapshotTop removeFromSuperview];
[snapshotBottom removeFromSuperview];
[transitionContext completeTransition:finished];
}];
}

Here are the main takeaways from this function:

  • We use iOS’s snapshot API to slice up the original scene and animate the parts of it around the screen
  • Like all other animators, we need to:
    • Manage the UIView hierarchy for the transition
    • Use one, and only one, animation block
    • Call the completeTransition: function to signal the end of the transition

Let’s more closely examine that first part. The snapshot API allows us to take a ‘screenshot’ of all or part of our current view and represent it as a new view. That new view can behave just like any other UIView object. Keep in mind that although the snapshot view can be thought of as an image representation of the original view, it is not a UIImageView.

Let’s take a look at the new transition:

transition2

Testing the snapshots in the simulator

This post only shows the code for TTPushAnimator because TTPopAnimator is so similar. A link for all the code is available below.


The code for this section is available in the ‘Step2_snapshotAPI’ directory here.


The next parts in this series can be read here and here.

2 thoughts on “iOS View Controller Transitions (Part 1)

  1. You pointed that we need to “Use one, and only one, animation block”. Why? What I need to do, if for example I want to animate collection view cells in transition or table rows and use some kind of repeatable animation blocks for several views?

Leave a comment