iOS Animation Tutorial: Custom View Controller Presentation Transitions

Learn how to create custom view controller presentation transitions and spice up the navigation of your iOS apps! By Fabrizio Brancati.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 3 of this article. Click here to view the first page.

Creating your Transition Animator

Open PopAnimator.swift. This is where you’ll add the code to transition between the two view controllers.

First, add the following properties to this class:

let duration = 0.8
var presenting = true
var originFrame = CGRect.zero

You’ll use duration in several places, such as when you tell UIKit how long the transition will take and when you create the constituent animations.

You also define presenting to tell the animator class whether you are presenting or dismissing a view controller. You want to keep track of this because, typically, you’ll run the animation forward to present and in reverse to dismiss.

Finally, you will use originFrame to store the original frame of the image the user taps — you will need that to animate from the original frame to a full screen image and vice versa. Keep an eye out for originFrame later on when you fetch the currently selected image and pass its frame to the animator instance.

Now you can move on to the UIViewControllerAnimatedTransitioning methods.

Replace the code inside transitionDuration(using:) with the following:

return duration

Reusing the duration property lets you easily experiment with the transition animation. You can simply modify the value of the property to make the transition run faster or slower.

Setting your Transition’s Context

It’s time to add some magic to animateTransition(using:). This method has one parameter, of type UIViewControllerContextTransitioning, which gives you access to the parameters and view controllers of the transition.

Before you start working on the code itself, it’s important to understand what the animation context actually is.

When the transition between the two view controllers begins, the existing view is added to a transition container view and the new view controller’s view is created but not yet visible, as illustrated below:

Therefore your task is to add the new view to the transition container within animateTransition(using:), “animate in” its appearance and “animate out” the old view if required.

By default, the old view is removed from the transition container when the transition animation is done.

Before you get too many cooks in this kitchen, you’ll create a simple transition animation to see how it works before implementing a much cooler, albeit more complicated, transition.

Adding an Expand Transition

You’ll start with a simple expand transition to get a feel for custom transitions. Add the following code to animateTransition(using:). Don’t worry about the two initialization warnings that pop up; you’ll be using these variables in just a minute:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!

First, you get the containerView where your animations will take place, and then you fetch the new view and store it in toView.

The transition context object has two very handy methods that give you access to the transition players:

  • view(forKey:): This lets you access the views of the “old” and “new” view controllers via the arguments UITransitionContextViewKey.from or UITransitionContextViewKey.to respectively.
  • viewController(forKey:): This lets you access the “old” and “new” view controllers via the arguments UITransitionContextViewControllerKey.from or UITransitionContextViewControllerKey.to respectively.

At this point, you have both the container view and the view to be presented. Next, you need to add the view to be presented as a child to the container view and animate it in some way.

Add the following to animateTransition(using:):

containerView.addSubview(toView)
toView.transform = CGAffineTransform(scaleX: 0.0, y: 0.0)
UIView.animate(
  withDuration: duration,
  animations: {
    toView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
  },
  completion: { _ in
    transitionContext.completeTransition(true)
  }
)

Note that you call completeTransition(_:) on the transition context in the animation completion block. This tells UIKit that your transition animations are done and that UIKit is free to wrap up the view controller transition.

Build and run your app and tap one of the recipes in the list and you’ll see the recipe overview expand in over the main view controller:

The transition is acceptable and you’ve seen what to do in animateTransition(using:) — but you’re going to add something even better!

Adding a Pop Transition

You’re going to structure the code for the new transition slightly differently, so replace all the code in animateTransition(using:) with the following:

let containerView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let recipeView = presenting ? toView : transitionContext.view(forKey: .from)!

containerView is where your animations will live, while toView is the new view to present. If you’re presenting, recipeView is simply the toView, otherwise you fetch it from the context since it’s now the “from” view. For both presenting and dismissing, you’ll always animate recipeView. When you present the details controller view, it will grow to take up the entire screen. When dismissed, it will shrink to the image’s original frame.

Add the following to animateTransition(using:):

let initialFrame = presenting ? originFrame : recipeView.frame
let finalFrame = presenting ? recipeView.frame : originFrame

let xScaleFactor = presenting ?
  initialFrame.width / finalFrame.width :
  finalFrame.width / initialFrame.width

let yScaleFactor = presenting ?
  initialFrame.height / finalFrame.height :
  finalFrame.height / initialFrame.height

In the code above, you detect the initial and final animation frames and then calculate the scale factor you need to apply on each axis as you animate between each view.

Now, you need to carefully position the new view so it appears exactly above the tapped image. This will make it look like the tapped image expands to fill the screen.

Scaling the View

Add the following to animateTransition(using:):

let scaleTransform = CGAffineTransform(scaleX: xScaleFactor, y: yScaleFactor)

if presenting {
  recipeView.transform = scaleTransform
  recipeView.center = CGPoint(
    x: initialFrame.midX,
    y: initialFrame.midY)
  recipeView.clipsToBounds = true
}

recipeView.layer.cornerRadius = presenting ? 20.0 : 0.0
recipeView.layer.masksToBounds = true

When presenting the new view, you set its scale and position so it exactly matches the size and location of the initial frame. You also set the correct corner radius.

Now, add the final bits of code to animateTransition(using:):

containerView.addSubview(toView)
containerView.bringSubviewToFront(recipeView)

UIView.animate(
  withDuration: duration,
  delay:0.0,
  usingSpringWithDamping: 0.5,
  initialSpringVelocity: 0.2,
  animations: {
    recipeView.transform = self.presenting ? .identity : scaleTransform
    recipeView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    recipeView.layer.cornerRadius = !self.presenting ? 20.0 : 0.0
  }, completion: { _ in
    transitionContext.completeTransition(true)
})

This will first add toView to the container. Next, you need to make sure the recipeView is on top since that’s the only view you’re animating. Remember that, when dismissing, toView is the original view so, in the first line, you’ll be adding it on top of everything else, and your animation will be hidden away unless you bring recipeView to the front.

Then, you can kick off the animations. Using a spring animation here will give it a bit of bounce.

Inside the animations expression, you change the transform, position, and corner radius of recipeView. When presenting, you’re going from the small size of the recipe image to the full screen so the target transform is just the identity transform. When dismissing, you animate it to scale down to match the original image size.

At this point, you’ve set the stage by positioning the new view controller over the tapped image, you’ve animated between the initial and final frames, and finally, you’ve called completeTransition(using:) to hand things back to UIKit. It’s time to see your code in action!

Build and run your app. Tap the first recipe image to see your view controller transition in action.

Well, it’s not perfect, but once you take care of a few rough edges your animation will be exactly what you wanted!