As Seen in the facebook and Google+ Apps: Switch to Dashboard and Sliding Notifications Animations

EDIT: There was a bug in the implementation because of a missing line in the the presentViewController:(UIViewController*)controllerToPush withDirection (PresentationDirection)direction method in CustomNavigationController.m.

In case of PresentationDirection PresentationDirectionOutViaScale and PresentationDirectionOutToBottom, we are hiding the overlying view and displaying the underlying one. This is equivalent to a popToViewController method. We need to add a call [self popToViewController:controllerToPush animated:NO] at the very end of those cases, to make sure that we won’t be keeping ‘dead’ ViewControllers on the navigation stack.
I’ve already updated the tutorial and source code accordingly. :-)

Now to the original post:
It’s time for a how-to post! :-)
The result of what we will be coding today is this:

In the FixVegas app I am currently developing, I decided to have a dashboard, similar to what facebook and Google+ for iPhone have. I really like the transitions animations when you click an icon in the dashboard but also when you press the button on the navigation bar to get back to the dashboard. Why? Because it gives the user a feeling of orientation in the app. The dashboard is an overlooking screen, so navigating to it via a regular back animation would rather put in in a linear setting. The scaling up animation instead perfectly sustains this idea of an element on a higher level in the navigation hierarchy.

I decided to implement it and since I was already working on view transitions, I also decided to add the slide in from the bottom/slide out to the bottom animation the apps use to display the notification screen.
In the following I will explain how I did it and also share the entire code with you. You can just grab the classes and add them to your project, scroll down to the bottom of the post to get them.

The difficulty with this task is that it’s not an animation you can just set as a property when you push or pop a view controller onto/from the stack. What you need to do to actually make it work is to add the view of the view controller that you want to push as a subview of the current top view controller, animate it and then push/pop the new view controller *without* animating it.

So, step by step:

First of all I created a class called “ViewAnimator”, of which the static methods, as the name says, receive a UIView object and perform transformations on it. The scale animation is done with Core Animation (so you will need to import the QuartzCore framework), while the sliding animation is done with simple UIView animation blocks.

Let’s take a look at the animation scaling the view up:

+(void)animateScaleUp:(UIView *)viewToScale withDuration:(NSTimeInterval)duration{
    
    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"];
    anim.timingFunction = [CAMediaTimingFunction 
                                 functionWithName:kCAMediaTimingFunctionEaseOut];
    anim.duration = duration;
    anim.repeatCount = 1;
    anim.autoreverses = NO;
    anim.removedOnCompletion = NO;
    anim.toValue = [NSValue valueWithCATransform3D:
                                 CATransform3DMakeScale(1.0, 1.0, 1.0)];
    
    [viewToScale.layer addAnimation:anim forKey:nil];
}

It basically gets a view and, from whatever scale that view has at that moment, scales it up to 100% (1.0).

Similarly, the scale down does the opposite.

The sliding animation goes like this:

+(void)animateSlideView:(UIView*)viewToSlide toFrame:(CGRect)finalPosition 
           withDuration:(NSTimeInterval)duration{
     
     [UIView beginAnimations:@"slide" context:NULL];
     [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
     [UIView setAnimationDuration:duration];
     
     viewToSlide.frame = finalPosition;
     
     [UIView commitAnimations];
}

I just reposition the view to whatever new position frame I want, so this function is reusable for any kind of sliding transition, no matter what direction it has.

Next, I create my CustomNavigationController, a subclass of UINavigationController, which will handle the view switching.

In the .h I define an enumeration for the different transition types I want to have:

typedef enum {
    PresentationDirectionInViaScale,
    PresentationDirectionOutViaScale,
    PresentationDirectionInFromBottom,
    PresentationDirectionOutToBottom
} PresentationDirection;

In the .m I have my core method that handles the view switching for the different transition types via if-then0else. It’s called
-(void)presentViewController:(UIViewController*)controllerToPush withDirection (PresentationDirection)direction;.

The very first thing I do is that I set the frame of the view of the controller I want to push to be the same as of the currently visible one, so that we have the exact same center point, otherwise the views would appear misplaced.

In via scale:
– I see my dashboard
– I make the view tiny with the downscale method of my ViewAnimator
– I add this new view as a subview of my top view controller’s view
– I bring this newly added view to the front
– I animate the view to scale up with my ViewAnimator class
– I reset its scale to 1.0
– After a delay equal to the duration of the animation, I push the actual view controller without animating it

//set the new view to have the same size and position as the currently shown one
    
    controllerToPush.view.frame = self.topViewController.view.frame;
    
    if(direction == PresentationDirectionInViaScale){
        //scale it down
        [self downScale:controllerToPush];
        
        //add it as a subview of the currently visible view and bring it to front
        [self.topViewController.view addSubview:controllerToPush.view];
        [self.topViewController.view bringSubviewToFront:controllerToPush.view];
        
        //now animate it so that it becomes visible
        [ViewAnimator animateScaleUp:controllerToPush.view];
        
        //reset the scale
        [self performSelector:@selector(resetScale:) withObject:controllerToPush
                afterDelay:kStandardAnimationDuration-0.02];
        
        //finally push the actual VC without animating it
        [self performSelector:@selector(showViewController:) withObject:controllerToPush
                afterDelay:kStandardAnimationDuration];
        
    }

Out via scale: This works the other way round, I first push the new view controller and then do the actual animation.
– I add the view of my current top view controller as a subview to the view of the view controller to be pushed
– I bring this subview to the front
– Now I push the new view controller, containing my old view as a subview, *without* animation
– I animate the “old” subview with my scaleDown: method from my ViewAnimator class
– Finally I remove the old view from its super view after the necessary delay.

else if(direction == PresentationDirectionOutViaScale){
        //add it as a subview of the currently visible view
        UIView *view = self.topViewController.view;
        [controllerToPush.view addSubview:view];
        [controllerToPush.view bringSubviewToFront:view];
        
        //push the actual VC without animating it
        [self showViewController:controllerToPush];
        
        //now animate the top subview so that it goes away
        [ViewAnimator performSelector:@selector(animateScaleDown:) withObject:view afterDelay:0.02];
        

        [view performSelector:@selector(removeFromSuperview) withObject:nil 
                 afterDelay:kStandardAnimationDuration-0.02];
        
        [self popToViewController:controllerToPush animated:NO];
    }

Slide in/out also works by adding views as subviews, similarly as before. Additionally, in the slide in, I define the direction by setting the initial frame y=the current’s frame Y+its height and the frame of the final position with y=0. For the slide out it’s the other way round.

else if(direction == PresentationDirectionInFromBottom){
        
        // change the frame position so that it is below the currently visible view
        CGRect oldFrame = controllerToPush.view.frame;
        controllerToPush.view.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y+oldFrame.size.height,
                                                         oldFrame.size.width, oldFrame.size.height);
        
        //add it as a subview of the currently visible view
        [self.topViewController.view addSubview:controllerToPush.view];
        [self.topViewController.view bringSubviewToFront:controllerToPush.view];
        
        //do the animation
        [ViewAnimator animateSlideView:controllerToPush.view toFrame:oldFrame];
        
        //finally push the actual VC without animating it
         [self performSelector:@selector(showViewController:) withObject:controllerToPush
                 afterDelay:kStandardAnimationDuration];
        
    }else if(direction == PresentationDirectionOutToBottom){
        
       
        CGRect oldFrame = controllerToPush.view.frame;
        CGRect newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y+oldFrame.size.height,
                    oldFrame.size.width, oldFrame.size.height);
        
        //add current controller's view and bring to front
        UIView *view = self.topViewController.view;
        [controllerToPush.view addSubview:view];
        [controllerToPush.view bringSubviewToFront:view];
        
        //push the actual VC without animating it
        [self showViewController:controllerToPush];
        
        //do the animation
        [ViewAnimator animateSlideView:view toFrame:newFrame];
        
        [view performSelector:@selector(removeFromSuperview) withObject:nil
                  afterDelay:kStandardAnimationDuration-0.02];
        [self popToViewController:controllerToPush animated:NO];
    }

What the CustomNavigationController class also includes is some methods that allow you to hide the title in the navigation bar. Another post will follow soon detailing how that works, for now just enjoy the code :-)

To use these animations, all you have to do is to
– add these two classes in your project (ViewAnimator, CustomNavigationController)
– set the type of your navigation controller (either in IB or in your code) to CustomNavigationController
– call -(void)presentViewController:(UIViewController*)controllerToPush withDirection:(PresentationDirection)direction; with the appropriate transition type

Example:

//show another view
[self.navigationController presentViewController:postsVC withDirection:PresentationDirectionInViaScale];

//show dashboard
[self.navigationController presentViewController:dashboardVC 
                                        withDirection:PresentationDirectionOutViaScale];
 
//slide in notifications 
[self.navigationController presentViewController:notificationsVC withDirection:PresentationDirectionInFromBottom];

//hide notifications and show dashboard
[self.navigationController presentViewController:dashboardVC withDirection:PresentationDirectionOutToBottom];

Here’s the full source code:

ViewAnimator.h
//
//  ViewAnimator.h
//  
//
//  Created by Irina Anastasiu on 13/09/11.
//  Copyright 2011 iriphon.com. All rights reserved.
//

#import <Foundation/Foundation.h>

#define kStandardAnimationDuration 0.3

@interface ViewAnimator : NSObject


+(void)animateScaleUp:(UIView *)viewToScale;
+(void)animateScaleUp:(UIView *)viewToScale withDuration:(NSTimeInterval)duration;

+(void)animateScaleDown:(UIView *)viewToScale;
+(void)animateScaleDown:(UIView *)viewToScale withDuration:(NSTimeInterval)duration;

+(void)animateSlideView:(UIView*)viewToSlide toFrame:(CGRect)finalPosition;
+(void)animateSlideView:(UIView*)viewToSlide toFrame:(CGRect)finalPosition withDuration:(NSTimeInterval)duration;
@end
ViewAnimator.m
//
//  ViewAnimator.m
//  
//
//  Created by Irina Anastasiu on 13/09/11.
//  Copyright 2011 iriphon.com. All rights reserved.
//

#import "ViewAnimator.h"
#import <QuartzCore/QuartzCore.h>

@implementation ViewAnimator

- (id)init
{
    self = [super init];
    if (self) {
        // Initialization code here.
    }
    
    return self;
}


+(void)animateScaleUp:(UIView *)viewToScale{
    [ViewAnimator animateScaleUp:viewToScale withDuration:kStandardAnimationDuration];
}

+(void)animateScaleDown:(UIView *)viewToScale{
    [ViewAnimator animateScaleDown:viewToScale withDuration:kStandardAnimationDuration];
}

+(void)animateSlideView:(UIView*)viewToSlide toFrame:(CGRect)finalPosition{
    [ViewAnimator animateSlideView:viewToSlide toFrame:finalPosition withDuration:kStandardAnimationDuration];
    
}


+(void)animateSlideView:(UIView*)viewToSlide toFrame:(CGRect)finalPosition withDuration:(NSTimeInterval)duration{
     
     [UIView beginAnimations:@"slide" context:NULL];
     [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
     [UIView setAnimationDuration:duration];
     
     viewToSlide.frame = finalPosition;
     
     [UIView commitAnimations];
}



+(void)animateScaleUp:(UIView *)viewToScale withDuration:(NSTimeInterval)duration{
    
    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"];
    anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
    anim.duration = duration;
    anim.repeatCount = 1;
    anim.autoreverses = NO;
    anim.removedOnCompletion = NO;
    anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)];
    
    [viewToScale.layer addAnimation:anim forKey:nil];
}


+(void)animateScaleDown:(UIView *)viewToScale withDuration:(NSTimeInterval)duration{
    
    CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"];
    anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
    anim.duration = duration;
    anim.repeatCount = 1;
    anim.autoreverses = NO;
    anim.removedOnCompletion = NO;
    anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.01, 0.01, 1.0)];
    
    [viewToScale.layer addAnimation:anim forKey:nil];
}

@end
CustomNavigationController.h
//
//  CustomNavigationController.h
//  
//
//  Created by Irina Anastasiu on 13/09/11.
//  Copyright 2011 iriphon.com. All rights reserved.
//

#import <UIKit/UIKit.h>


typedef enum {
    PresentationDirectionInViaScale,
    PresentationDirectionOutViaScale,
    PresentationDirectionInFromBottom,
    PresentationDirectionOutToBottom
} PresentationDirection;

@interface CustomNavigationController : UINavigationController <UINavigationControllerDelegate>


-(void)presentViewController:(UIViewController*)controllerToPush withDirection:(PresentationDirection)direction;
@end
CustomNavigationController.m
//
//  CustomNavigationController.m
//  
//
//  Created by Irina Anastasiu on 13/09/11.
//  Copyright 2011 iriphon.com. All rights reserved.
//

#import <QuartzCore/QuartzCore.h>
#import "CustomNavigationController.h"
#import "ViewAnimator.h"

@interface CustomNavigationController (Private)
-(void)showViewController:(UIViewController*)controllerToPush;
-(void)resetScale:(UIViewController*)controllerToPush;
-(void)resetScaleView:(UIView*)view;
-(void)downScale:(UIViewController*)controllerToPush;
-(void)downScaleView:(UIView*)view;
@end

@implementation CustomNavigationController

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        // Initialization code here.
        self.delegate = self;
    }
    
    return self;
}

#pragma mark - Navigation Controller Delegate

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated{
    navigationController.topViewController.navigationItem.title = nil;
    navigationController.navigationItem.title = nil;
}

#pragma mark - Helpers for Transitions

-(void)presentViewController:(UIViewController*)controllerToPush withDirection:(PresentationDirection)direction{
    
    //set the new view to have the same size and position as the currently shown one
    
    controllerToPush.view.frame = self.topViewController.view.frame;
    
    if(direction == PresentationDirectionInViaScale){
        //scale it down
        [self downScale:controllerToPush];
        
        //add it as a subview of the currently visible view
        [self.topViewController.view addSubview:controllerToPush.view];
        [self.topViewController.view bringSubviewToFront:controllerToPush.view];
        
        //now animate it so that it becomes visible
        [ViewAnimator animateScaleUp:controllerToPush.view];
        
        //reset the scale
        [self performSelector:@selector(resetScale:) withObject:controllerToPush afterDelay:kStandardAnimationDuration-0.02];
        
        //finally push the actual VC without animating it
        [self performSelector:@selector(showViewController:) withObject:controllerToPush afterDelay:kStandardAnimationDuration];
        
    }else if(direction == PresentationDirectionOutViaScale){
        //add it as a subview of the currently visible view
        UIView *view = self.topViewController.view;
        [controllerToPush.view addSubview:view];
        [controllerToPush.view bringSubviewToFront:view];
        
        //push the actual VC without animating it
        [self showViewController:controllerToPush];
        
        //now animate the top subview so that it goes away
        [ViewAnimator performSelector:@selector(animateScaleDown:) withObject:view afterDelay:0.02];
        

        [view performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:kStandardAnimationDuration-0.02];
        
        [self popToViewController:controllerToPush animated:NO];

    }else if(direction == PresentationDirectionInFromBottom){
        
        // change the frame position so that it is below the currently visible view
        CGRect oldFrame = controllerToPush.view.frame;
        controllerToPush.view.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y+oldFrame.size.height, oldFrame.size.width, oldFrame.size.height);
        
        //add it as a subview of the currently visible view
        [self.topViewController.view addSubview:controllerToPush.view];
        [self.topViewController.view bringSubviewToFront:controllerToPush.view];
        
        //do the animation
        [ViewAnimator animateSlideView:controllerToPush.view toFrame:oldFrame];
        
        //finally push the actual VC without animating it
         [self performSelector:@selector(showViewController:) withObject:controllerToPush afterDelay:kStandardAnimationDuration];
        
    }else if(direction == PresentationDirectionOutToBottom){
        
       
        CGRect oldFrame = controllerToPush.view.frame;
        CGRect newFrame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y+oldFrame.size.height, oldFrame.size.width, oldFrame.size.height);
        
        //add current controller's view and bring to front
        UIView *view = self.topViewController.view;
        [controllerToPush.view addSubview:view];
        [controllerToPush.view bringSubviewToFront:view];
        
        //push the actual VC without animating it
        [self showViewController:controllerToPush];
        
        //do the animation
        [ViewAnimator animateSlideView:view toFrame:newFrame];
        
        [view performSelector:@selector(removeFromSuperview) withObject:nil afterDelay:kStandardAnimationDuration-0.02];
        
        [self popToViewController:controllerToPush animated:NO];
    }
}


-(void)showViewController:(UIViewController*)controllerToPush {
   
    if ([self.viewControllers containsObject:controllerToPush]) {
        [self popToViewController:controllerToPush animated:NO];
    }else{
        [self pushViewController:controllerToPush animated:NO];
    }
}

-(void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
    
    UIBarButtonItem *newBackButton = [[UIBarButtonItem alloc] initWithTitle:@"Back" style: UIBarButtonItemStyleBordered target: nil action: nil];
    
	[viewController.navigationItem setBackBarButtonItem: newBackButton];
	[newBackButton release];
    viewController.title = nil;
    self.topViewController.title = @"Back";
    
    [super pushViewController:viewController animated:animated];
    
}

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated{
    

    UIBarButtonItem *newBackButton = [[UIBarButtonItem alloc] initWithTitle:@"Back" style: UIBarButtonItemStyleBordered target: nil action: nil];
    
	[viewController.navigationItem setBackBarButtonItem: newBackButton];
	[newBackButton release];
    viewController.navigationItem.title = nil;
    self.topViewController.title = nil;
    return [super popToViewController:viewController animated:animated];
}


-(void)resetScale:(UIViewController*)controllerToPush{
    
    controllerToPush.view.transform = CGAffineTransformMakeScale(1.0,1.0);    
}

-(void)resetScaleView:(UIView*)view{
    
    view.transform = CGAffineTransformMakeScale(1.0,1.0);    
}

-(void)downScale:(UIViewController*)controllerToPush{
    
    controllerToPush.view.transform = CGAffineTransformMakeScale(0.1,0.1);    
}

-(void)downScaleView:(UIView*)view{
    
    view.transform = CGAffineTransformMakeScale(0.1,0.1);    
}


@end