Custom Animations Between UIViewControllers

Apple has really done a lot to make it easy to use multiple UIViewController objects and to move between them easily with some stock animations. The “normal” behaviors for iPhone applications are relatively straightforward to achieve — some requiring just a line or two of code.

But what if you want to do something different? For example, you are working on a game that has it’s own navigation hierarchy. Not only are you not using the normal UINavigationController usage model, but you also want to do something different visually besides sliding in the new view? Read on for one possible way of handling this.

The method that I am using involves having a custom root view controller object that manages the transitions between the other view controllers. The way I have it setup is that there is never more than 2 view controllers active at once — the root controller and whichever controller you are currently using. I’ll also point out that for this example, I am not using Interface Builder or XIB files. Everything is generated using code. Feel free to experiment on your own if you want to use IB.

To start, you will need to setup your root view controller class. This is simply a subclass of UIViewController:

    @interface MyRootController : UIViewController {
        UIViewController *activeController;
    }
    -(id)initWithController:(UIViewController *)aController;
    -(void)loadView;
    -(void)dealloc;
    -(void)crossfadeTo:(UIViewController *)aController duration:(float)aDuration;
    @end

We are going to implement a simple crossfade effect in this class — the crossfadeTo:duration: method will handle properly fading between the two views and properly disposing of the old view controller when done.

Let’s take a look at the initWithController: method next:

    -(id)initWithController:(UIViewController *)aController {
        if(!(self = [super initWithNibName:nil bundle:nil])) {
                return nil;
        }
        activeController = [aController retain];
        return self;
    }

As you can see, it is very basic — all we are doing here is just keeping the initial controller around so that we can use it later. The real work begins in loadView:

    -(void)loadView {
        self.view = [[[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
 
        [activeController viewWillAppear:NO];
        [self.view addSubview:activeController.view];
        [activeController viewDidAppear:NO];
    }

The first thing we do is obtain a new UIView object that covers the screen. This is the view that will be used as the parent view for the active controller’s view. It will also hold the second controller’s view during an animation.

The next part is to hold up to the contract of UIViewController. Whenever a view will appear or disappear, there are four methods in UIViewController that need to be called: viewWillAppear:, viewDidAppear:, viewWillDisappear: and viewDidDisappear:. The parameter for all of these is a BOOL indicating if there is an animation happening. Since we are not animating the first view onto the screen, we say NO.

The dealloc method is basically the reverse of both the loadView and initWithController methods — we are just cleaning up all the toys we got out:

    -(void)dealloc {
        [activeController viewWillDisappear:NO];
        [activeController.view removeFromSuperview];
        [activeController viewDidDisappear:NO];
 
        [activeController release];
        [super dealloc];
    }

Notice that we are calling viewWillDisappear: and viewDidDisappear: to keep up our end of the contract again.

At this point, we have enough code for our view to show up on the screen and for us to not leak any resources when we are done with the view controller. But that’s not why we are here — we’re here to do some custom animations between two view controllers. Let’s take a look at how to do a crossfade:

    -(void)crossfadeTo:(UIViewController *)aController duration:(float)aDuration {
        [aController viewWillAppear:YES];
        [activeController viewWillDisappear:YES];
 
        aController.view.alpha = 0.0f;
        [self.view addSubview:aController.view];
 
        [aController viewDidAppear:YES];
 
        [UIView beginAnimations:@"crossfade" context:nil];
        [UIView setAnimationDuration:aDuration];
        aController.view.alpha = 1.0f;
        activeController.view.alpha = 0.0f;
        [UIView commitAnimations];
 
        [self performSelector:@selector(animationDone:) withObject:aController afterDelay:aDuration];
    }
 
    -(void)animationDone:(UIViewController *)aNewViewController {
        [activeController.view removeFromSuperview];
        [activeController viewDidDisappear:YES];
        [activeController release];
 
        activeController = [aNewViewController retain];
    }

There are two parts to this code — first we start the animation in crossfadeTo:duration: and then finish it up in animationDone:. The intent with animationDone: is that you should be able to reuse this method for whatever animations you feel like implementing. All of the animation setup is done in crossfadeTo:duration:. Let’s take a closer look at that code:

    [aController viewWillAppear:YES];
    [activeController viewWillDisappear:YES];

The first part is just about keeping the contract again. In this case, we have both a view that is about to appear and another one that is about to disappear. Notice that we are passing YES since we are going to be doing an animation this time.

    aController.view.alpha = 0.0f;
    [self.view addSubview:aController.view];

Next we add our new view to our parent view with the alpha set to zero. The new view needs to be a part of the view hierarchy for it to eventually get to the screen, and we don’t want it covering up the current view yet.

    [aController viewDidAppear:YES];

Since we added our new view, we need to let the view controller that it’s now visible. Even though the alpha is set to zero at this point, it’s part of the view hierarchy and is active. Therefore, we need to make sure the controller knows.

    [UIView beginAnimations:@"crossfade" context:nil];
    [UIView setAnimationDuration:aDuration];
    aController.view.alpha = 1.0f;
    activeController.view.alpha = 0.0f;
    [UIView commitAnimations];

This part is the animation code. All we are really doing for the animation is just changing the alpha on our new view from zero to one and the alpha on the old view from one to zero. We set the duration to the passed in value and then kick off the animation with the commitAnimations call.

    [self performSelector:@selector(animationDone:) withObject:aController afterDelay:aDuration];

This last bit of code is to kick off a call to animationDone: when the animation is actually done. There are methods in UIView to setup a callback method (setAnimationDelegate: and setAnimationWillEndSelector:), but when I attempted to use them with the 2.2 SDK, I ran into issues with the callback being called at the wrong time. If you are using a newer SDK, you might be able to use these methods, but the performSelector:withObject:afterDelay: method should work just as well for our needs.

The second part of the transition is in animationDone: method. It has two parts — the first part is to finish up with our view controller we just transitioned off the screen:

    [activeController.view removeFromSuperview];
    [activeController viewDidDisappear:YES];
    [activeController release];

We are simply removing the view, letting the view controller that we did remove it and finally releasing the controller.

    activeController = [aNewViewController retain];

On the new controller side, we are now finally going to retain the new object. We haven’t had to retain it up until this point because it was always being retained for us — the performSelector:withObject:afterDelay: method retains the object that you pass to the withObject: parameter.

And with this, our crossfade animation is complete and we now have a new active view controller. If you’ve been playing along at home, you should be able to see this for yourself.

To use your new view controller, simply do this in your appDidFinishLaunching::

    window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    controller = [[MyRootController alloc] initWithController:[[[MyFirstViewController alloc] init] autorelease]];
    [window addSubview:controller.view];

And when you want to crossfade between two views, do something like this:

    [controller crossfadeTo:[[[MySecondViewController alloc] init] autorelease] duration:2.0f];

I’ll leave it as an exercise for the reader as to how to create MyFirstViewController and MySecondViewControler. These are just subclasses of UIViewController.

I hope that this gets you started with doing different animations between view controllers than simply the stock animations that Apple provides with the UIViewController class.

For more information regarding the classes that I’ve used above, please take a look at these links:
http://developer.apple.com/iphone/library/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html
http://developer.apple.com/iPhone/library/documentation/UIKit/Reference/UIView_Class/UIView/UIView.html


About this entry