Custom UIViewController Transitions: Getting Started
This tutorial will teach you to create custom UIViewController transitions for presenting and dismissing, and how to make them interactive! By Richard Critz.
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
Custom UIViewController Transitions: Getting Started
25 mins
iOS delivers some nice view controller transitions — push, pop, cover vertically — for free but it’s great fun to make your own. Custom UIViewController
transitions can significantly enhance your users’ experiences and set your app apart from the rest of the pack. If you’ve avoided making your own custom transitions because the process seems too daunting, you’ll find that it’s not nearly as difficult as you might expect.
In this tutorial, you’ll add some custom UIViewController
transitions to a small guessing game app. By the time you’ve finished, you’ll have learned:
- How the transitioning API is structured.
- How to present and dismiss view controllers using custom transitions.
- How to build interactive transitions.
UIView
animations, so you’ll need a basic working knowledge of them. If you need help, check out our tutorial on iOS Animation for a quick introduction to the topic.
Getting Started
Download the starter project. Build and run the project; you’ll see the following guessing game:
The app presents several cards in a page view controller. Each card shows a description of a pet and tapping a card reveals which pet it describes.
Your job is to guess the pet! Is it a cat, dog or fish? Play with the app and see how well you do.
The navigation logic is already in place but the app currently feels quite bland. You’re going to spice it up with custom transitions.
Exploring the Transitioning API
The transitioning API is a collection of protocols. This allows you to make the best implementation choice for your app: use existing objects or create purpose-built objects to manage your transitions. By the end of this section, you’ll understand the responsibilities of each protocol and the connections between them. The diagram below shows you the main components of the API:
The Pieces of the Puzzle
Although the diagram looks complex, it will feel quite straightforward once you understand how the various parts work together.
Transitioning Delegate
Every view controller can have a transitioningDelegate
, an object that conforms to UIViewControllerTransitioningDelegate
.
Whenever you present or dismiss a view controller, UIKit asks its transitioning delegate for an animation controller to use. To replace a default animation with your own custom animation, you must implement a transitioning delegate and have it return the appropriate animation controller.
Animation Controller
The animation controller returned by the transitioning delegate is an object that implements UIViewControllerAnimatedTransitioning
. It does the “heavy lifting” of implementing the animated transition.
Transitioning Context
The transitioning context object implements UIViewControllerContextTransitioning
and plays a vital role in the transitioning process: it encapsulates information about the views and view controllers involved in the transition.
As you can see in the diagram, you don’t implement this protocol yourself. UIKit creates and configures the transitioning context for you and passes it to your animation controller each time a transition occurs.
The Transitioning Process
Here are the steps involved in a presentation transition:
- You trigger the transition, either programmatically or via a segue.
- UIKit asks the “to” view controller (the view controller to be shown) for its transitioning delegate. If it doesn’t have one, UIKIt uses the standard, built-in transition.
- UIKit then asks the transitioning delegate for an animation controller via
animationController(forPresented:presenting:source:)
. If this returnsnil
, the transition will use the default animation. - UIKit constructs the transitioning context.
- UIKit asks the animation controller for the duration of its animation by calling
transitionDuration(using:)
. - UIKit invokes
animateTransition(using:)
on the the animation controller to perform the animation for the transition. - Finally, the animation controller calls
completeTransition(_:)
on the transitioning context to indicate that the animation is complete.
The steps for a dismissing transition are nearly identical. In this case, UIKit asks the “from” view controller (the one being dismissed) for its transitioning delegate. The transitioning delegate vends the appropriate animation controller via animationController(forDismissed:)
.
Creating a Custom Presentation Transition
Time to put your new-found knowledge into practice! Your goal is to implement the following animation:
- When the user taps a card, it flips to reveal the second view scaled down to the size of the card.
- Following the flip, the view scales to fill the whole screen.
Creating the Animator
You’ll start by creating the animation controller.
From the menu, select File\New\File…, choose iOS\Source\Cocoa Touch Class, and click Next. Name the file FlipPresentAnimationController, make it a subclass of NSObject and set the language to Swift. Click Next and set the Group to Animation Controllers. Click Create.
Animation controllers must conform to UIViewControllerAnimatedTransitioning
. Open FlipPresentAnimationController.swift and update the class declaration accordingly.
class FlipPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
}
Xcode will raise an error complaining that FlipPresentAnimationController
does not conform to UIViewControllerAnimatedTransitioning
. Click Fix to add the necessary stub routines.
You’re going to use the frame of the tapped card as a starting point for the animation. Inside the body of the class, add the following code to store this information.
private let originFrame: CGRect
init(originFrame: CGRect) {
self.originFrame = originFrame
}
Next, you must fill in the code for the two stubs you added. Update transitionDuration(using:)
as follows:
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 2.0
}
As the name suggests, this method specifies the duration of your transition. Setting it to two seconds will prove useful during development as it leaves enough time to observe the animation.
Add the following to animateTransition(using:)
:
// 1
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to),
let snapshot = toVC.view.snapshotView(afterScreenUpdates: true)
else {
return
}
// 2
let containerView = transitionContext.containerView
let finalFrame = transitionContext.finalFrame(for: toVC)
// 3
snapshot.frame = originFrame
snapshot.layer.cornerRadius = CardViewController.cardCornerRadius
snapshot.layer.masksToBounds = true
Here’s what this does:
- Extract a reference to both the view controller being replaced and the one being presented. Make a snapshot of what the screen will look like after the transition.
- UIKit encapsulates the entire transition inside a container view to simplify managing both the view hierarchy and the animations. Get a reference to the container view and determine what the final frame of the new view will be.
- Configure the snapshot’s frame and drawing so that it exactly matches and covers the card in the “from” view.
Continue adding to the body of animateTransition(using:)
.
// 1
containerView.addSubview(toVC.view)
containerView.addSubview(snapshot)
toVC.view.isHidden = true
// 2
AnimationHelper.perspectiveTransform(for: containerView)
snapshot.layer.transform = AnimationHelper.yRotation(.pi / 2)
// 3
let duration = transitionDuration(using: transitionContext)
The container view, as created by UIKit, contains only the “from” view. You must add any other views that will participate in the transition. It’s important to remember that addSubview(_:)
puts the new view in front of all others in the view hierarchy so the order in which you add subviews matters.
- Add the new “to” view to the view hierarchy and hide it. Place the snapshot in front of it.
- Set up the beginning state of the animation by rotating the snapshot 90˚ around its y-axis. This causes it to be edge-on to the viewer and, therefore, not visible when the animation begins.
- Get the duration of the animation.
AnimationHelper
is a small utility class responsible for adding perspective and rotation transforms to your views. Feel free to have a look at the implementation. If you’re curious about the magic of perspectiveTransform(for:)
, try commenting out the call after you finish the tutorial.
You now have everything set up; time to animate! Complete the method by adding the following.
// 1
UIView.animateKeyframes(
withDuration: duration,
delay: 0,
options: .calculationModeCubic,
animations: {
// 2
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 1/3) {
fromVC.view.layer.transform = AnimationHelper.yRotation(-.pi / 2)
}
// 3
UIView.addKeyframe(withRelativeStartTime: 1/3, relativeDuration: 1/3) {
snapshot.layer.transform = AnimationHelper.yRotation(0.0)
}
// 4
UIView.addKeyframe(withRelativeStartTime: 2/3, relativeDuration: 1/3) {
snapshot.frame = finalFrame
snapshot.layer.cornerRadius = 0
}
},
// 5
completion: { _ in
toVC.view.isHidden = false
snapshot.removeFromSuperview()
fromVC.view.layer.transform = CATransform3DIdentity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
Here’s the play-by-play of your animation:
- You use a standard UIView keyframe animation. The duration of the animation must exactly match the length of the transition.
- Start by rotating the “from” view 90˚ around its y-axis to hide it from view.
- Next, reveal the snapshot by rotating it back from its edge-on state that you set up above.
- Set the frame of the snapshot to fill the screen.
- The snapshot now exactly matches the “to” view so it’s finally safe to reveal the real “to” view. Remove the snapshot from the view hierarchy since it’s no longer needed. Next, restore the “from” view to its original state; otherwise, it would be hidden when transitioning back. Calling
completeTransition(_:)
informs UIKit that the animation is complete. It will ensure the final state is consistent and remove the “from” view from the container.
Your animation controller is now ready to use!