Specializing View Controller Animated Transitions in iOS 7

Our Rosetta Stone mobile development team's goal is to write engaging apps to help users learn languages, increase literacy and improve their mental fitness. In this post, I'm going to talk about the "engaging" part and how we improved user experience with the iOS version of one of our apps.

Animation as a clue to users about where they are and what they should be doing has become table stakes in iOS app development. There are a lot of cool tools that have been provided by Apple, and it gets easier every year to do great stuff.

One of our apps, Advanced English for Business, has this data structure: The user has a list of "goals". Each goal has a list of "lessons" and each lesson consists of a series of "activities". All of these items are represented to the user in different view controllers that present lists of our data objects, and users can navigate up and down in this structure. This tree hierarchy can be confusing to navigate. Where am I? What am I doing right now? How can I get to the thing I want to do?

We do a lot to help users out here (clear styling and language), but we wanted to add animations that would cross the view controller transitions between Goals, Lessons and Activities to make the connections between these items clear. We're going to write this kind of animation several times, so we want to be able to reuse code. We also want the app to look modern and to be fun to use (engagement!).

OK, so there's our task. We want a generic, reusable way of making these inter-view controller animated transitions, because we have several of them. Fortunately, Apple's provided tools to do this. I'm not going to talk about how to use the tools here (there is an excellent writeup about how this works in objc.io's issue #5 and another in Colin Eberhardt's blog post on ScottLogic. If you are not familiar, you may want to read up before continuing). Instead, I'm going to talk about how we decided to take our set of requirements and make our own little framework for making many of these view controller animated transitions quickly.

First, in order to do this kind of animation, we have to be able to vend an object that implements the UIViewControllerAnimatedTransitioning protocol. Cool. But that protocol is super generic. We have a more specific scenario. We want to do animations from one view (like a UICollectionViewCell or a UIButton) in the "from" view controller to another, matching view in the "to" controller. To get an idea of our requirements, you can view one of the final view controller animations from the app below.



If we wrote a custom UIViewControllerAnimatedTransitioning implementing object for each transition, there would be a lot of code that looked the same! We're always going to be animating one view from one position to another.

We decided to turn it inside out. We will have a generic UIViewControllerAnimatedTransitioning type object, and will customize it using objects that implement a protocol that we design.

So, we have two protocols that represent our "from" and "to" view controllers and the unique things they bring to the animation.

@protocol RSViewControllerAnimationSourceProtocol <NSObject>

@required

- (RSViewControllerAnimationType)supportedSourceType;

- (UIView *)sourceView;

- (CGRect)sourceAnimationFrame;

- (void)preAnimationSetupWithView:(UIView *)animatedView
             sourceViewController:(UIViewController *)sourceViewController
             targetViewController:(UIViewController *)targetViewController;

- (void)postAnimationSetupWithView:(UIView *)animatedView
              sourceViewController:(UIViewController *)sourceViewController
              targetViewController:(UIViewController *)targetViewController;

@optional

- (NSTimeInterval)transitionDuration;

- (void)willStartSourceAnimation;

- (void)didEndSourceAnimation;

@end

What's going on here?

supportedSourceType returns an enum value; but it's essentially a way for us to match target view controllers with matching source view controllers. If both the target and source protocols offer the same value here, they are a match! This will be clearer later on when we discuss how to put this all together in an app.

sourceView returns a UIView which we want to animate. Our generic animation class does not care what this view is at all. For us, sometimes it's a UICollectionViewCell, sometimes a snapshot UIImageView, but who cares? Note: this UIView should not be part of any existing UIView hierarchies!

sourceAnimationFrame is the frame where we are going to start that view.

preAnimationSetupWithView:sourceViewController:targetViewController: lets the view controller do any, well, pre animation setup of the view. For example, if we are animating a UICollectionViewCell, we'll want to make the View Controller's version of that cell disappear (i.e. set the alpha to 0.0) to aid in the illusion that the cell is moving from one view to the next.

postAnimationSetupWithView:sourceViewController:targetViewController: lets the view controller set the final state of the animation. Maybe you need to adjust a frame or some NSLayoutConstraint to change the view slightly during the animation.

We've also got some optional methods. transitionDuration allows you to control how long the animation takes. And, we provide a couple of callbacks (willStartSourceAnimation and didEndSourceAnimation) to let the view controller do some clean up if needed (remember when I suggested you set a UICollectionViewCell's alpha value to 0.0. Well, you should probably set it back.)

You will be able to (correctly) conclude that the "source" or "from" view controller is in charge of the animation in our little framework here. So, the corresponding "target" or "to" protocol is simpler.

@protocol RSViewControllerAnimationTargetProtocol <NSObject>

@required

- (RSViewControllerAnimationType)supportedTargetType;

- (CGRect)targetAnimationFrame;

@optional

- (void)willStartTargetAnimation;

- (void)didEndTargetAnimation;

@end

supportedTargetType fulfills the same function as supportedSourceType does in the RSViewControllerAnimationSourceProtocol.

targetAnimationFrame is the frame where we are going to end the animated view.

Finally, the "target" also gets a couple of callbacks. willStartTargetAnimation and didEndTargetAnimation also allow for some setup and teardown if needed.

OK, so we want our source and target view controllers to implement these protocols. How does it come together? In our UIViewControllerContextTransitioning implementing class! Here we go...

@interface RSViewControllerAnimationTransitioning ()

@property (nonatomic, weak) id<RSViewControllerAnimationSourceProtocol> sourceDelegate;
@property (nonatomic, weak) id<RSViewControllerAnimationTargetProtocol> targetDelegate;

@end


@implementation RSViewControllerAnimationTransitioning

#pragma mark - UIViewControllerAnimatedTransitioning

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIView *container = [transitionContext containerView];

    UIView *animatedView = [self.sourceDelegate sourceView];
    
    CGRect sourceRect = [self.sourceDelegate sourceAnimationFrame];
    
    CGRect targetRect = [self.targetDelegate targetAnimationFrame];
    
    if ([self.sourceDelegate respondsToSelector:@selector(willStartSourceAnimation)]) {
        [self.sourceDelegate willStartSourceAnimation];
    }

    if ([self.targetDelegate respondsToSelector:@selector(willStartTargetAnimation)]) {
        [self.targetDelegate willStartTargetAnimation];
    }
    
    [container addSubview:toViewController.view];
    [container addSubview:animatedView];

    animatedView.frame = sourceRect;

    NSTimeInterval duration = [self transitionDuration:transitionContext];
 
    [self.sourceDelegate preAnimationSetupWithView:animatedView
        sourceViewController:fromViewController
        targetViewController:toViewController];

    [UIView animateWithDuration:duration
                          delay:0
                        options:UIViewAnimationOptionCurveEaseInOut
                     animations:^{
                         [self.sourceDelegate postAnimationSetupWithView:animatedView
                                                    sourceViewController:fromViewController
                                                    targetViewController:toViewController];

                         animatedView.frame = targetRect;
                         
                         [animatedView layoutIfNeeded];
                     }
                     completion:^(BOOL finished) {
                         if ([self.sourceDelegate respondsToSelector:@selector(didEndSourceAnimation)]) {
                             [self.sourceDelegate didEndSourceAnimation];
                         }

                         if ([self.targetDelegate respondsToSelector:@selector(didEndTargetAnimation)]) {
                             [self.targetDelegate didEndTargetAnimation];
                         }

                         [animatedView removeFromSuperview];

                         [transitionContext completeTransition:finished];
                     }];
}

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    if (self.sourceDelegate && [self.sourceDelegate respondsToSelector:@selector(transitionDuration)]) {
         return [self.sourceDelegate transitionDuration];
    }
    return kRSViewControllerAnimationTransitionDuration;
}

Here's the flow: * Grab our animated views and the source and target frames * Notify the view controllers that we are about to run a animation * Set up the animation * Run the animation * Notify the view controllers that we are finished with our animation * Clean up the animated view * Call completeTransition on our transitionContext object. (This last bit is very important!)

OK, we now have an object that can run the animated transition between the two view controllers and interfaces for those view controllers to implement. How to we attach these objects together and connect them to the transition?

We have a little factory class method on our RSViewControllerAnimationTransitioning class!

+ (RSViewControllerAnimationTransitioning *)viewControllerAnimationTransitioningFactoryFromViewController:(UIViewController *)sourceVC
                                                                                         toViewController:(UIViewController *)targetVC
{
    if ([sourceVC conformsToProtocol:@protocol(RSViewControllerAnimationSourceProtocol)]
        && [targetVC conformsToProtocol:@protocol(RSViewControllerAnimationTargetProtocol)]) {

        if (((id<RSViewControllerAnimationSourceProtocol>)sourceVC).supportedSourceType == ((id<RSViewControllerAnimationTargetProtocol>)targetVC).supportedTargetType) {
            
            RSViewControllerAnimationTransitioning *transitioning = [[RSViewControllerAnimationTransitioning alloc] init];

            transitioning.sourceDelegate = (id<RSViewControllerAnimationSourceProtocol>)sourceVC;
            transitioning.targetDelegate = (id<RSViewControllerAnimationTargetProtocol>)targetVC;
            
            return transitioning;
        }
    }
    return nil;
}

This is pretty simple. If each source and target view controller implements their respective protocols, and they both vend the same RSViewControllerAnimationType values, then we create a RSViewControllerAnimationTransitioning setup with the correct interfaces to run the transition!

The last part of connecting this is simple. If you are using a UINavigationViewController, there's a delegate method called navigationController:animationControllerForOperation:fromViewController:toViewController:. We set up our factory method there!

- (id<UIViewControllerAnimatedTransitioning>)
                        navigationController:(UINavigationController *)navigationController
             animationControllerForOperation:(UINavigationControllerOperation)operation
                          fromViewController:(UIViewController *)fromVC
                            toViewController:(UIViewController *)toVC
{
    return [RSViewControllerAnimationTransitioning viewControllerAnimationTransitioningFactoryFromViewController:fromVC
                                                                                                toViewController:toVC];
}

This will return nil if there's no valid transition to make, otherwise a RSViewControllerAnimationTransitioning object to do the animation.

If you do not have a UINavigationViewController, then you can also set up a transitioning delegate for your view controller. In that delegate, there's a very similar method:

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                  presentingController:(UIViewController *)presenting
                                                                      sourceController:(UIViewController *)source
{
    return [RSViewControllerAnimationTransitioning viewControllerAnimationTransitioningFactoryFromViewController:source 
                                                                                                toViewController:presented];
}

We are done. Furthermore, additional animations are even simpler to add. You just need to implement the source and target protocols in your from and to view controllers to hook into this infrastructure.

This kind of animation is really helpful in letting users know how your application works, where they are hierarchically, and to have fun while using your app! Please let us know if you have questions, corrections, or feedback.

David Coufal

dcoufal@rosettastone.com


David Coufal is a Lead iOS Software Engineer at Rosetta Stone in Boulder, CO. He is a Colorado native, graduate of Caltech and MIT and 15-year veteran of the software engineering industry. davidcoufal.com.


Rosetta Stone has development offices in Boulder, CO, San Francisco, CA, Seattle, WA, Austin, TX and Harrisonburg, VA. We are always looking for talented engineers so please check out jobs.rosettastone.com if you want to come join us help the world learn!