Think And Build

iOS

How to create custom ViewController Transitions

Posted on .

How to create custom ViewController Transitions

Introduction

My holidays are so close I can already imagine myself swimming with dolphins in the blue. But I can’t leave without leaving (pun intended) a good read to you, my beloved readers.

If you take a look at my previous posts you can guess I’m particularly interested in UI customization. With this article I want to go a step further and talk about an inexplicably neglected topic. “Custom transitions”. Neglected because I can’t find any recent article about that, so , by the way, if you have any good resource at hand, please, do share it. 🙂

Here’s a preview of what we’ll create with this article:

Introduction

We’re going to build a custom Container ViewController that presents its children controller’s view performing an animation.
The animation I want to perform has to be automatically calculated independently by the Container children structure. So we animate the entire view, the same way the UINavigationController does when it pushes/pops a controller.

Before starting I suggest you to take a fast look at my previous post about Custom Container Controllers, where I show how to build a really simple navigation through controllers. Also remember to download the source code (you can find it at the end of the article).

Structure

Our Container Controller will be really simple because I want to keep you focused on the Transition. It just has to load a child controller and switch the current view with the next view performing a transition. Therefore we don’t keep track of any hierarchy or anything, we just load/unload show/hide View Controllers.

The code

Open the project and you can locate 3 viewControllers.
ContainerViewController is our container and StepONEViewController and StepTWOViewcontroller are the controllers that we are going to present with the container.

Let’s focus on the ContainerViewController interface.


@interface ContainerViewController : UIViewController

@property (weak, nonatomic) IBOutlet UIView *detailView;
@property (strong, nonatomic) UIViewController *currentViewController;

- (id)initWithViewController:(UIViewController*)viewController;
- (void)presentViewController:(UIViewController *)viewController;

@end 

We find a reference to the current view controller (currentViewController) and a view to host the current view (detailView).

The function initWithViewController is a simple shortcut to initialize the containerViewController with a child.
Last, the presentViewController is where all the magic happens.

Here’s a scheme that describes how the properties are related with the child controllers.

As you can see opening the file StepONEViewController.xib there is a button at the center of the view. This button is connected with the action goToStepTwo. This is the code:


- (IBAction)goToStepTwo:(id)sender {
    StepTWOViewController *stepTWOViewController = [[StepTWOViewController alloc] init];
    ContainerViewController *containerController = (ContainerViewController*) self.parentViewController;
    
    [containerController presentViewController:stepTWOViewController];
}

This function just initializes an instance of StepTWOViewController and it calls the function presentViewController of the ContainerViewController. The stepONEViewController is a child of ContainerViewController and we can access its parent with the standard property parentViewController. A similar technique is used for StepTWOController.

The tricks behind the transition

Finally we can enter the core of this tutorial!
Open the file ContainerViewController and check the function presentViewController.

Before digging into the code, let’s summarize what this function does:

A) It initializes the Custom Container hierarchy
B) It builds fake views with screenshots of the Current and the Next controllers’ views
C) It animates the fake views
D) It performs the last operations on the container hierarchy

And now the code step by step (I use the same separation that I’ve used in the previous list).


- (void)presentViewController:(UIViewController *)viewController{
    
    //A. Container hierarchy management ------------------------------
    
    //1. The current controller is going to be removed
    [self.currentViewController willMoveToParentViewController:nil];
    
    //2. The new controller is a new child of the container
    [self addChildViewController:viewController];
    
    //3. Setup the new controller's frame
    viewController.view.frame = CGRectMake(0, 0, self.detailView.frame.size.width, self.detailView.frame.size.height);

With this code we simply organize the Controller’s hierarchy (please refer to my previous article to get more information about how custom containers work) and we are sure that the new view fits the detailView’s size exactly.


    //B. Generate images and set up views (Screenshots) ------------------------
    
    //4. Create a screenshot of the CURRENT view and setup its layer
    UIImageView *currentView = [self takeScreenshot:self.view.layer];
    //4b. Build a view with black bg and attach here the just taken screenshot 
    UIView *blackView = [self blackView];
    [blackView addSubview:currentView];
    
    CGRect oldFrame = [currentView.layer frame];
    currentView.layer.anchorPoint = CGPointMake(0,0.5);
    currentView.layer.frame = oldFrame;
    
    //5. Hide the CURRENT view (we've taken the screenshot)
    [self.currentViewController.view setHidden:YES];

    //6. Add the new view to the detail view
    [self.detailView addSubview:viewController.view];
    
    //7. Create a screenshot of the NEXT view and setup its layer
    UIImageView *nextView = [self takeScreenshot:self.view.layer];
    oldFrame = [nextView.layer frame];
    nextView.layer.anchorPoint = CGPointMake(0,0.5);
    nextView.layer.frame = oldFrame;
    nextView.frame = CGRectMake(-self.view.frame.size.width, 0, nextView.frame.size.width, nextView.frame.size.height);
    //7.b Attach the screen shot to the black background view
    [blackView addSubview:nextView];
    [self.view addSubview:blackView];

    
    //C. THE ANIMATION!!!! ------------------------------------------
    
    //8. Setup the NEXT view layer
    CATransform3D tp = CATransform3DIdentity;
    tp.m34 = 1.0/ -500;
    tp = CATransform3DTranslate(tp, -300.0f, -10.0f, 300.0f);
    tp = CATransform3DRotate(tp, radianFromDegree(20), 0.0f,1.0f, 0.8f);
    nextView.layer.transform = tp;
    nextView.layer.opacity = 0.0f;
    
    //9. Finally, perform the animation from PREVIOUS to NEXT view
    [UIView animateWithDuration:1.0
     
    //9b. Animate the views to create a transition effect
    animations:^{
        
        //9c. Create transition for the CURRENT view. 
        CATransform3D t = CATransform3DIdentity;
        t.m34 = 1.0/ -500;
        t = CATransform3DRotate(t, radianFromDegree(5.0f), 0.0f,0.0f, 1.0f);
        t = CATransform3DTranslate(t, viewController.view.frame.size.width * 2, 0.0f, -400.0);
        t = CATransform3DRotate(t, radianFromDegree(-45), 0.0f,1.0f, 0.0f);
        t = CATransform3DRotate(t, radianFromDegree(50), 1.0f,0.0f, 0.0f);
        currentView.layer.transform = t;
        currentView.layer.opacity = 0.0;
        
        //9d. Create transition for the NEXT view. 
        CATransform3D t2 = CATransform3DIdentity;
        t2.m34 = 1.0/ -500;
        t2 = CATransform3DTranslate(t2, viewController.view.frame.size.width, 0.0f, 0.0);
        nextView.layer.transform = t2;
        nextView.layer.opacity = 1.0;

    }


    //D. Container hierarchy management ------------------------------
    //10. At the end of the animations we remove the previous view and update the Controller hierarchy.
    completion:^(BOOL finished) {
        
        //Tell the controller that it's going to be removed
        [self.currentViewController beginAppearanceTransition:NO animated:YES];
        
        //Remove the old Detail Controller view from superview
        [self.currentViewController.view removeFromSuperview];
     
        //Remove the old Detail controller from the hierarchy
        [self.currentViewController removeFromParentViewController];
     
        //Set the new view controller as current
        self.currentViewController = viewController;
        [self.currentViewController didMoveToParentViewController:self];
     
        //The Black backogrund view is no more needed
        [blackView removeFromSuperview];
     
    }];
    
       
}

As I told you, the step B is all about the generation of a “fake” representation of our views. Essentially we create a view with a black background and then we attach the screenshots of our Current and Next Controller’s view to it.

At step 4 we take a screenshot of the Container Controller with the current view.
At step 4b we attach this screenshot to the black view .
At step 5 we hide the Current view and we attach the next view over it. Now at step 7 we can create a new screenshot with the next view and again attach it to the black view.
At step 7b you can see that the nextView frame is attached at the left of currentView because we want to perform an animation from left to right (you can play with these configurations to create many other transition styles).

Check this really long scheme which summarizes the flow that we have just seen.

To create the screenshots we use the function takeScreenshot:. This function creates an imageContext and renders in it the desired layer, it converts the context into an image and it builds a UIImageView with this image.


//Create a UIImageView from the given layer
- (UIImageView*)takeScreenshot:(CALayer*)layer{
    
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, [[UIScreen mainScreen] scale]);
    [layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIImageView *screnshot = [[UIImageView alloc] initWithImage:image];
    
    return screnshot;
}

With the step C we finally perform the transition! At step 8 we perform some pre-animation settings over nextView, which is the screenshot of the View controller that is going to be loaded into the Container controller. We basically translate/rotate it in the 3D Space (if you need to now how 3D works on layers check this tutorial) assigning a non linear position and ending up with a more interesting animation.

The step C is where animations are applied.
At step 9b currentView (the screenshot of the controller’s view we have to unload from the controller) is translated outside of the screen and is rotated on x and y axis. We also play a fadeOut effect changing the layer opacity.
While at step 9c nextView is translated to the right final position and a fadeIn effect is applied to it too.

The step D is the completion block. At this point the animations are done and the views should be at their final positions.
We perform operations on the Controller hierarchy to remove the old controller and we can now delete the BlackView where the 2 screenshots are attached, showing the real Controller’s view.

You’ve probably noticed that the “Next View” has been attached at its final destination at step 6 and it has ever been at the right position. We have just created a fake animation that has taken place over the real interface.

Conclusions

You could obviously modify the behavior of the Container Controller building something similar to a UINavigationController. For example you can add a navigation stack heading animation that goes left or right depending on push or pop actions.

I recently made some experiments with the shouldRasterize property of CALayer. It could probably substitute the manual screenshot that I’ve shown here but I’ll tell you something more in the next articles when I’m sure about how to implement the same effect with it.

Thanks to Sam Howzit for the Hippo image 😛

and to the unknown author of the great landscape picture!

Thank you for reading!

Yari D'areglia

Yari D'areglia

https://www.thinkandbuild.it

Senior iOS developer @ Neato Robotics by day, game developer and wannabe artist @ Black Robot Games by night.

Navigation