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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
iOS Animation Tutorial: Custom View Controller Presentation Transitions
20 mins
- Getting Started
- Behind the Scenes of Custom Transitions
- Implementing Transition Delegates
- Wiring up the Delegates
- Using the Animator
- Creating your Transition Animator
- Setting your Transition’s Context
- Adding an Expand Transition
- Adding a Pop Transition
- Scaling the View
- Adding Some Polish
- Adding a Dismiss Transition
- Device Orientation Transition
- Where to Go From Here?
Whether you’re presenting the camera view controller or one of your own custom-designed modal screens, it’s important to understand how these transitions are happening.
Transitions are always called with the same UIKit method: present(_:animated:completion:)
. This method “gives up” the current screen to another view controller using the default presentation animation to slide the new view up to cover the current one.
The illustration below shows a “New Contact” view controller sliding up over the list of contacts:
In this iOS animation tutorial, you’ll create your own custom presentation controller transitions to replace the default one and liven up this tutorial’s project.
Getting Started
Download the project materials using the Download Materials button at the top or bottom of this tutorial.
Open the starter project and select Main.storyboard to begin the tour:
The first view controller, HomeViewController
, contains the app’s recipe list. HomeViewController
presents DetailsViewController
whenever the user taps one of the images in the list. This view controller sports an image, a title and a description.
There’s already enough code in HomeViewController.swift and DetailsViewController.swift to support the basic app. Build and run the app to see how the app looks and feels:
Tap on one of the recipe image, and the details screen comes up via the standard vertical cover transition. That might be OK, but your recipes deserve better!
Your job is to add some custom presentation controller animations to your app to make it blossom! You’ll replace the current stock animation with one that expands the tapped recipe image to a full-screen view like so:
Roll up your sleeves, put your developer apron on and get ready for the inner workings of custom presentation controllers!
Behind the Scenes of Custom Transitions
UIKit lets you customize your view controller’s presentation via the delegate pattern. You simply make your main view controller, or another class you create specifically for that purpose, conform to UIViewControllerTransitioningDelegate
.
Every time you present a new view controller, UIKit asks its delegate whether or not it should use a custom transition. Here’s what the first step of the custom transitioning dance looks like:
UIKit calls animationController(forPresented:presenting:source:)
to see if it returns a UIViewControllerAnimatedTransitioning
object. If that method returns nil
, UIKit uses the built-in transition. If UIKit receives a UIViewControllerAnimatedTransitioning
object instead, then UIKit uses that object as the animation controller for the transition.
There are a few more steps in the dance before UIKit can use the custom animation controller:
UIKit first asks your animation controller — simply known as the animator — for the transition duration in seconds, then calls animateTransition(using:)
on it. This is when your custom animation gets to take center stage.
In animateTransition(using:)
, you have access to both the current view controller on the screen as well as the new view controller to be presented. You can fade, scale, rotate and manipulate the existing view and the new view however you like.
Now that you’ve learned a bit about how custom presentation controllers work, you can start to create your own.
Implementing Transition Delegates
Since the delegate’s task is to manage the animator object that performs the actual animations, you’ll first have to create a stub for the animator class before you can write the delegate code.
From Xcode’s main menu, select File ▸ New ▸ File… and choose the template iOS ▸ Source ▸ Cocoa Touch Class.
Name the new class PopAnimator, make sure Swift is selected, and make it a subclass of NSObject.
Open PopAnimator.swift and update the class definition to make it conform to the UIViewControllerAnimatedTransitioning
protocol as follows:
class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
}
You’ll see some complaints from Xcode since you haven’t implemented the required delegate methods. You can either use the quick fix that Xcode provides to generate the missing stubbed methods, or write them out yourself.
Add the following method to the class:
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?)
-> TimeInterval {
return 0
}
The 0
value above is just a placeholder value. You’ll replace this later with a real value as you work through the project.
Now, add the following method stub to the class:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
}
The above stub will hold your animation code, adding it should have cleared the remaining errors in Xcode.
Now that you have the basic animator class, you can move on to implementing the delegate methods on the view controller side.
Wiring up the Delegates
Open HomeViewController.swift and add the following extension to the end of the file:
// MARK: - UIViewControllerTransitioningDelegate
extension HomeViewController: UIViewControllerTransitioningDelegate {
}
This code indicates that the view controller conforms to the transitioning delegate protocol, which you’ll add here in a moment.
First, find prepare(for:sender:)
in HomeViewController.swift. Near the bottom of that method, you’ll see the code that sets up the details view controller. detailsViewController
is the instance of the new view controller, and you need to set HomeViewController
as its transitioning delegate.
Add the following line right before setting the recipe:
detailsViewController.transitioningDelegate = self
Now UIKit will ask HomeViewController
for an animator object every time you present the details view controller on the screen. However, you still haven’t implemented any of the UIViewControllerTransitioningDelegate
methods, so UIKit will still use the default transition.
The next step is to actually create your animator object and return it to UIKit when requested.
Using the Animator
Add the following new property at the top of HomeViewController
:
let transition = PopAnimator()
This is the instance of PopAnimator
that will drive your animated view controller transitions. You only need one instance of PopAnimator
since you can continue to use the same object each time you present a view controller, as the transitions are the same every time.
Now, add the first delegate method to the UIViewControllerTransitioningDelegate extension in HomeViewController
:
func animationController(
forPresented presented: UIViewController,
presenting: UIViewController, source: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
return transition
}
This method takes a few parameters that let you make an informed decision whether or not you want to return a custom animation. In this tutorial, you’ll always return your single instance of PopAnimator
since you have only one presentation transition.
You’ve already added the delegate method for presenting view controllers, but how will you deal with dismissing one?
Add the following delegate method to handle this:
func animationController(forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
return nil
}
The method above does essentially the same thing as the previous one: You check which view controller was dismissed and decide whether to return nil
and use the default animation or to return a custom transition animator and use that instead. At the moment, you return nil
, as you aren’t going to implement the dismissal animation until later.
You finally have a custom animator to take care of your custom transitions. But does it work?
Build and run your app and tap one of the recipe images:
Nothing happens. Why? You have a custom animator to drive the transition, but… oh, wait, you haven’t added any code to the animator class! You’ll take care of that in the next section.
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
orUITransitionContextViewKey.to
respectively. -
viewController(forKey:): This lets you access the “old” and “new” view controllers via the arguments
UITransitionContextViewControllerKey.from
orUITransitionContextViewControllerKey.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!
Adding Some Polish
Currently, your animation starts from the top-left corner. That’s because the default value of originFrame
has the origin at (0, 0)
, and you never set it to any other value.
Open HomeViewController.swift and add the following code to the top of animationController(forPresented:presenting:source:)
before the code returns the transition:
guard
let selectedIndexPathCell = tableView.indexPathForSelectedRow,
let selectedCell = tableView.cellForRow(at: selectedIndexPathCell)
as? RecipeTableViewCell,
let selectedCellSuperview = selectedCell.superview
else {
return nil
}
transition.originFrame = selectedCellSuperview.convert(selectedCell.frame, to: nil)
transition.originFrame = CGRect(
x: transition.originFrame.origin.x + 20,
y: transition.originFrame.origin.y + 20,
width: transition.originFrame.size.width - 40,
height: transition.originFrame.size.height - 40
)
transition.presenting = true
selectedCell.shadowView.isHidden = true
This gets the selected cell, sets the originFrame
of the transition to the frame of selectedCellSuperview
, which is the cell you last tapped. Then, you set presenting to true and hide the tapped cell during the animation.
Build and run the app again and tap different recipes in the list to see how your transition looks for each.
Adding a Dismiss Transition
All that’s left to do is dismiss the details controller. You’ve actually done most of the work in the animator already — the transition animation code does the logic juggling to set the proper initial and final frames, so you’re most of the way to play the animation both forward and backward. Sweet!
Open HomeViewController.swift and replace the body of animationController(forDismissed:)
with the following:
transition.presenting = false
return transition
This tells your animator object that you’re dismissing a view controller so the animation code will run in the correct direction.
Build and run the app to see the result. Tap on a recipe and then tap on the X button on the top left of the screen to dismiss it.
The transition animation looks great, but notice the recipe you picked has disappeared from the table view! You’ll need to make sure the tapped image re-appears when you dismiss the details screen.
Open PopAnimator.swift and add a new closure property to the class:
var dismissCompletion: (() -> Void)?
This will let you pass in some code to run when the dismiss transition completes.
Next, find animateTransition(using:)
and add the following code to the completion handler in the call to animate(...)
, right before the call to completeTransition()
:
if !self.presenting {
self.dismissCompletion?()
}
This code executes dismissCompletion
once the dismiss animation has finished, which is the perfect spot to show the original image.
Open HomeViewController.swift and add the following code to the main class at the beginning of the file:
override func viewDidLoad() {
super.viewDidLoad()
transition.dismissCompletion = { [weak self] in
guard
let selectedIndexPathCell = self?.tableView.indexPathForSelectedRow,
let selectedCell = self?.tableView.cellForRow(at: selectedIndexPathCell)
as? RecipeTableViewCell
else {
return
}
selectedCell.shadowView.isHidden = false
}
}
This code displays the original image of the selected cell to replace the recipe details view controller once the transition animation completes.
Build and run your app to enjoy the transition animations both ways now that the recipes aren’t getting lost along the way!
Device Orientation Transition
You can think of device orientation changes as a presentation transition from a view controller to itself, just at a different size.
Since the app is built with Auto Layout, you don’t need to make changes. Simply rotate the device and enjoy the transitions (or press Command-left arrow if testing in the iPhone Simulator)!
Where to Go From Here?
You can download the finished project using the Download Materials button at the top or bottom of this tutorial.
If you enjoyed what you learned in this tutorial, why not check out iOS Animations by Tutorials book, available in our store?
You can also improve the transition that you’ve created in this tutorial with Reproducing Popular iOS Controls · App Store: Drag Down to Dismiss episode on how to create the App Store Today tab animation effect.
We hope you enjoy this update and stay tuned for more book releases and updates!
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more