2.5hours
iOS
Medium
Swift
Learn how to create custom animations to transition between UIViewControllers. Download the project code and walk through step-by-step, with detailed code breakdowns, to build a unique interactive transition animation in forward and reverse directions.
Overview
Quite a few apps out there have animated transitions between UIViewControllers. Some of them are certainly taking advantage of the animated transitions built-into UINavigationController. That component permits swiping from the edge of the screen to dismiss the current UIViewController.
Many developers want to put their own stamp on the look and feel of their app to make it stand out from the crowd. Apple has that base covered with the following bits of the SDK relating directly to creating a custom transition:
- UIViewControllerAnimatedTransitioning
- UIViewControllerTransitioningDelegate
- UIPercentDrivenInteractiveTransition
Refer to the animation below to get an idea of how the application that is built in this tutorial works:
The requirements to create a custom transition are:
- implement the UIViewControllerAnimatedTransitioning protocol
- implement the UIViewControllerTransitioningDelegate protocol
- extend the UIPercentDrivenInteractiveTransition class
UIKit does the heavylifting for these animated transitions. It manages all the interactions with the system and provides a perfect layer of abstraction to access lower-level animations. It is also the source of quite a few instances of magic seen later in this article.
Setup
Download the code for this project from the public repo on bitbucket.org Skip straight to the coding section by clicking here.
Components of Custom Transitions
Custom transition animations minimally require implementations of the UIViewControllerTransitioningDelegate and UIViewControllerAnimatedTransitioning protocols. Adding user interactivity with the animation requires subclassing UIPercentDrivenInteractiveTransition
UIViewControllerTransitioningDelegate
When a UIViewController invokes a segue, present(animated:completion), or dismiss(animated:completion), UIKit will query the dismissed UIViewController to see if there's a custom animation controller implementation of UIViewControllerTransitioningDelegate.
If a custom animation controller is not assigned, UIKit will perform the transition back to the original UIViewController without custom animations.
Apple's documentation provides additional details:
When implementing your transitioning delegate object, you can return different animator objects depending on whether a view controller is being presented or dismissed. All transitions use a transition animator object—an object that conforms to the UIViewControllerAnimatedTransitioning protocol—to implement the basic animations.
A transition animator object performs a set of animations over a finite period of time. If you want to use touch input or other user interactions to control the timing of the animation, you can also provide an interactive animator object—an object that conforms to the UIViewControllerInteractiveTransitioning protocol—to update the progress of the animations. You can provide separate animator objects for presenting and dismissing the view controller.
UIViewControllerAnimatedTransitioning
Implementing the protocol defines the animation controller returned by the UIViewControllerTransitioningDelegate. Animation parameters include duration, CGAffineTransform, from and to frames, and others that encompass the desired presentation and dismissal of a UIViewController
According to Apple's documentation:
The methods in this protocol let you define an animator object, which creates the animations for transitioning a view controller on or off screen in a fixed amount of time. The animations you create using this protocol must not be interactive. To create interactive transitions, you must combine your animator object with another object that controls the timing of your animations.
UIPercentDrivenInteractiveTransition
Subclass this concrete class to add user interactive animations between UIViewControllers. In other words, a custom subclass allows the user to slide from the edge of the screen to return to the previous UIViewController but also stop and return to the current UIViewController depending on manual user interaction.
According to Apple's documentation:
A percent-driven interactive transition object relies on a transition animator delegate—a custom object that adopts the UIViewControllerAnimatedTransitioning protocol—to set up and perform the animations. To use this concrete class, return an instance of it from your view controller delegate when asked for an interactive transition controller.
Summary
Given the protocol and class details provided thus far, the high-level steps to create the transition are summarized below:
- Every UIViewController must have a class variable of UIPercentDrivenInteractiveTransition to drive the manual interaction with the transition.
- Every UIViewController that can be swiped back to will implement the UIViewControllerTransitioningDelegate. The protocol method animationController(forPresented: presenting:source:) will return an instance of the custom UIViewControllerAnimatedTransitioning presentation animator. The method protocol method animationController(dismissed:) will return an instance of the custom animator depending on the requested transition.
- Subclass UIPercentDrivenInteractiveTransition and implement the method handleGesture(_ gestureRecognizer:) to perform custom manual interaction.
- Subclass NSObject and implement the UIViewControllerAnimatedTransitioning protocol to create a class that will present and a separate one that will dismiss a UIViewController. The method animateTransition(using transitionContext:) will define that custom animation.
Three UIViewControllers are used for the demonstration app; FirstViewController, SecondViewController, and ThirdViewController. The diagram below shows how these UIViewControllers implement the protocols and interact, based on this summary description.
The diagram demonstrates the following:
- The functions shown in each UIViewController are the implementation of the UIViewControllerTransitioningDelegate protocol, which return the appropriate UIViewControllerAnimatedTransitioning animator object when UIKit requests it.
- The animator object is either an instance of SlidePresentAnimationController or SlideDismissAnimationController. Both classes implement the UIViewControllerAnimatedTransitioning protocol.
- Each UIViewController has an instance of UIPercentDrivenInteractiveTransition named SlideInteractionController which will handle the manual slide interaction
Steps to Transition
The details provided thus far should shed light on how custom transitions work. Before jumping into code it is worthwhile to walk through how UIKit takes requests and delivers the custom animated transition. Here's a high-level diagram of the flow:
- Segue/Dismiss/Present is called from code to transition to another UIViewController.
- UIKit inquires the UIViewController that will be transitioned to and checks if it has implemented the UIViewControllerTransitioningDelegate If there is no protocol implementation by the UIViewController then the standard transition will occur.
- If the protocol is implemented, then UIKit calls the protocol function animationController(forPresented:) or animationController(forDismissed:) to receive an instance of UIViewControllerAnimatedTransitioning animator object. This object handles the animation and is of type SlidePresentAnimationController or SlideDismissAnimationController
- The animators, SlidePresentAnimationController or SlideDismissAnimationController, each have a method named transitionDuration(using:) that UIKit calls to determine the requested timing of the animation.
- UIKit calls the method animateTransition(using:) of UIViewControllerAnimatedTransitioning (SlidePresentAnimationController or SlideDismissAnimationController) and performs the animations defined therein.
- UIKit calls handleGesture(_ gestureRecognizer:) of UIPercentDrivenInteractiveTransition continuously during the transition. The implementation of that method will check the progress and handle different states of the gesture such as .began, .changed, and more.
The dismiss action is activated via a swipe from the left edge of the screen and uses the implementation of UIPercentDrivenInteractiveTransition. The animation to dismiss is driven by the SlideDismissAnimationController and the animation to present is controlled by SlidePresentAnimationController.
Last important point to make regarding UIPercentDrivenInteractiveTransition:
Think of it as an observer that acts as a middle-man, checking the state of the animation controlled manually by the user. The standard animation of the class that implements UIViewControllerAnimatedTransitioning for the dismiss action, SlideDismissAnimationController in this case, is run forward or back depending on the user's gesture. Its progress depends on the user's gesture and the code reacts accordingly to the state of user progress.
Build the Demo App
With the overview and requirements detailed, its time to create the demo app step-by-step. Detailed explanations will provide a good understanding of what's going in Interface Builder as well as under-the-hood in code.
Create the UIViewController Classes
Create three distinct subclasses of UIViewController:
- FirstViewController
- SecondViewController
- ThirdViewController
Select File->New->File in XCode, select Cocoa Touch Class, and give it the name "FirstViewController", making sure it's a subclass of UIViewController.
Repeat the same for the other two classes. More code will be added to them later.
Interface Builder Configuration
Pull two additional UIViewControllers onto Storyboard, set their background colors to three distinct and unique colors. Line them up next to each other, as shown in the image below.
For each of the UIViewControllers in Interface Builder:
- Select the top-most left button in the frame, named "View Controller" when hovering over it, then select the Identity Inspector.
- Assign each UIViewController the respective class created previously using the Class dropdown; FirstViewController for the first UIViewController, SecondViewController for the second in the lineup, and so on.
- Create the segues. Control-drag from the top-left button on the FirstViewController frame over to the SecondViewController.
- Select the segue and go to Attributes Inspector to apply an Identifier with the name of "swipesegue". Make sure the Kind is set to Show. Refer to the image below:
Repeat the same steps above but connect the SecondViewController to the ThirdViewController with a segue named "swipesegue2".
After tying up the Interface Builder UIViewControllers with custom classes and connecting them with named segues, give the SecondViewController and ThirdViewController StoryboardIDs:
- Select the top-left button in the SecondViewController frame then the Identity Inspector.
- Give it a Storyboard ID "secondviewcontroller".
- Do the same for the ThirdViewController giving it the Storyboard ID "thirdviewcontroller".
UIViewControllerAnimatedTransitioning
Create two new files by selecting File->New->File->Swift File, naming the first one SlideDismissAnimationController and the second SlidePresentAnimationController.
SlideDismissAnimationController
Insert the following code into the SlideDismissAnimationController. This code provides the animation to UIKit for dismissing a UIViewController:
import UIKit
class SlideDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning{
private let destinationFrame: CGRect
let interactionController: SlideInteractionController?
init(destinationFrame: CGRect, interactionController: SlideInteractionController?) {
self.destinationFrame = destinationFrame
self.interactionController = interactionController
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.7
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get a local reference to the from and to VCs and a snapshot of what the screen will look like after the transition (the toVC appearance).
guard let fromVC = transitionContext.viewController(forKey: .from), let toVC = transitionContext.viewController(forKey: .to),
let snapshot = toVC.view.snapshotView(afterScreenUpdates: true) else{
return
}
// Get the container view from transitionContext where animation is performed.
let container = transitionContext.containerView
// Get sizes of the VCs at the end of the transition from the context
toVC.view.frame = transitionContext.finalFrame(for: toVC)
fromVC.view.frame = transitionContext.finalFrame(for: fromVC)
// Setup the transforms
let offScreenRight = CGAffineTransform(translationX: container.frame.width, y: 0)
let offScreenLeft = CGAffineTransform(translationX: -container.frame.width/10, y: 0)
// start the toView to the left side of the visible animation area 1/10 the width of the container
toVC.view.transform = offScreenLeft
// add both views for animation to the container
container.addSubview(toVC.view)
container.addSubview(fromVC.view)
// Get the setting for duration from above
let duration = self.transitionDuration(using: transitionContext)
// Start the actual animation
UIView.animate(withDuration: duration,
delay: 0.0,
usingSpringWithDamping: 0.9,
initialSpringVelocity: 0.8,
options: .curveEaseOut,
animations: {
// animate the currently visible VC off to the right
fromVC.view.transform = offScreenRight
// return the VC we want visible now to it's original size/transformation
toVC.view.transform = CGAffineTransform.identity// CGAffineTransformIdentity
}) { finished in
// return both VCs to their original placement and size
toVC.view.transform = CGAffineTransform.identity
fromVC.view.transform = CGAffineTransform.identity
// If SlideInteractionController is enabled and the animation is manually cancelled
// make sure to remove the toVC otherwise it'll remain visible
if transitionContext.transitionWasCancelled {
toVC.view.removeFromSuperview()
}
// Tell context the animation is complete - based on whether the transitionContext cancelled
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
The code is documented with detailed comments but here's a high-level breakdown of the key components:
- interactionController: the class that extends UIPercentDrivenInteractiveTransition and assists with the manual interaction by the user during the transition. An instance of this will be provided externally through the init() method.
- transitionDuration(transitionContext:): provides UIKit with an overall duration for the entire transition animation
- animateTransition(transitionContext:):
- captures references to the "to" UIViewController and the "from" UIViewController
- Prepares their positions prior to the animation
- Adds them to the containerView used by UIKit to encapsulate the animation.
- Applies CGAffineTransforms to animate them from their prepared positions to final state.
The snapshot is not used because the code directly manipulates the from and to UIViewControllers. For complex animations, use the snapshot then remove it from the superView when the animation is complete.
SlidePresentAnimationController
Insert the following code into the SlidePresentAnimationController. It provides the animation to UIKit for presenting a UIViewController:
import UIKit
class SlidePresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning{
private let originFrame: CGRect
let interactionController: SlideInteractionController?
init(originFrame: CGRect, interactionController: SlideInteractionController?) {
self.originFrame = originFrame
self.interactionController = interactionController
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get a local reference to the from and to VCs and a snapshot of what the screen will look like after the transition (the toVC appearance).
guard let fromVC = transitionContext.viewController(forKey: .from), let toVC = transitionContext.viewController(forKey: .to), let snapshot = toVC.view.snapshotView(afterScreenUpdates: true) else{
return
}
// Get the container view from transitionContext where animation is performed.
let container = transitionContext.containerView
// Setup the transform that will shrink the VCs
let shrink = CGAffineTransform.init(scaleX: 0.8, y: 0.8)
// start the toVC to the right of the visible screen
toVC.view.center.x += container.frame.width
// shrink the toVC that's off to the right
toVC.view.transform = shrink
// add both views for animation to the container
container.addSubview(fromVC.view)
container.addSubview(toVC.view)
// Get the setting for duration from above
let duration = self.transitionDuration(using: transitionContext)
// Start the actual animation
UIView.animateKeyframes(
withDuration: duration,
delay: 0,
options: .calculationModeLinear,
animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations: {
// Shrink the currently visible VC
fromVC.view.transform = shrink
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.25, animations: {
// move the currently visible VC off screen to the left
fromVC.view.center.x -= container.frame.width
// move the shrunk toVC that's off to the right of screen back to center
toVC.view.center.x -= container.frame.width
})
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
// return toVC to it's original size
toVC.view.transform = CGAffineTransform.identity
})
}, completion:{_ in
// return both VCs to their original placement and size
toVC.view.transform = CGAffineTransform.identity
fromVC.view.transform = CGAffineTransform.identity
// If SlideInteractionController is enabled and the animation is manually cancelled
// make sure to remove the toVC otherwise it'll remain visible
if transitionContext.transitionWasCancelled {
fromVC.view.removeFromSuperview()
}
// Tell context the animation is complete - based on whether the transitionContext cancelled
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
As with the SlideDismissAnimationController, the code is documented in detail but here's a high-level breakdown:
- interactionController: provides manual interaction by the user over the animation. Provided to the class externally via init()
- transitionDuration(transitionContext:): returns the total duration of the animation. Note that the present transition is more complex thus, the animation duration is longer than dismiss.
- animateTransition(transitionContext:):
- captures references to the to UIViewController and the from UIViewController
- sets up the shrink CGAffineTransform used to reduce the size of the to and from UIViewControllers as they are animated into place.
- positions the to UIViewController off screen to the right and applies the shrink CGAffineTransform to it.
- uses UIView.animateKeyframes to get fine-grained control over each step of this animation. First it shrinks the from UIViewController, using 1/4 of the total transitionDuration. Next keyframe animation moves both the to UIViewController and from UIViewController off the screen and centered in the screen respectively, using another 1/4 of the duration. Finally, the last keyframe uses the remaining time in the duration to return the to UIViewController to its normal size using the CGAffineTransform.identity.
UIPercentDrivenInteractiveTransition
The code for manual user interaction with the animation transition is required now. Tasks include:
- Creation of a class that extends the UIPercentDrivenInteractiveTransition
- Definition of gestures that will be monitored
- Use a method, handleGesture(gestureRecognizer:) to initiate the animations.
- Tell UIKit when the animation should be canceled or completed.
Create a new Swift file named SlideInteractionController.swift and add the following code:
import UIKit
class SlideInteractionController: UIPercentDrivenInteractiveTransition{
var interactionInProgress = false
private var shouldCompleteTransition = false
private weak var viewController: UIViewController!
init(viewController: UIViewController) {
super.init()
self.viewController = viewController
// Setup the dismiss gesture and add it to the incoming ViewController
let leftGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
leftGesture.edges = .left
viewController.view.addGestureRecognizer(leftGesture)
// Setup the dismiss gesture and add it to the incoming ViewController
let rightGesture = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
rightGesture.edges = .right
viewController.view.addGestureRecognizer(rightGesture)
}
Notice the weak reference to UIViewController as a class variable. That's the UIViewController that creates an instance of SlideInteractionController. All of the UIViewControllers that are part of a transition will get an instance of SlideInteractionController.
In the init() method there are two UIScreenEdgePanGestureRecognizerssetup. One is from the left edge of the screen and the other from the right edge. addGestureRecognizer() is used to add each of these to the UIViewController. It's the one that has a weak reference otherwise known as the "owner" of SlideInteractionController.
Next, add the following handleGesture(gestureRecognizer:) method call to SlideInteractionController:
@objc func handleGesture(_ gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: gestureRecognizer.view!.superview!)
var progress:CGFloat = 0.0
if gestureRecognizer.edges == .right{
// Swiping in from the right is a negative translation.x so get absolute value
progress = (abs(translation.x) / 400.0)
}else{
//Dividing by a high number reduces the progress and keeps the animation
//in-sync with the user finger.
progress = (translation.x / 1000.0)
}
progress = CGFloat(fminf(fmaxf(Float(progress), 0.0), 1.0))
// Switch the state of the gesture to determine what action to take
switch gestureRecognizer.state {
case .began:
// Forward transition
if gestureRecognizer.edges == .right{
interactionInProgress = true
setupAndPresentNextViewController()
// Back transition
}else{
interactionInProgress = true
viewController.dismiss(animated: true, completion: nil)
}
case .changed:
shouldCompleteTransition = progress > 0.35
update(progress)
case .cancelled:
interactionInProgress = false
cancel()
case .ended:
interactionInProgress = false
if shouldCompleteTransition {
finish()
} else {
cancel()
}
default:
break
}
}
The handleGesture(gestureRecognizer:) method performs the key functionality of the manual interaction, detailed below:
- Converts the gestureRecognizer translation into a progress number between 0-1. The swipe in from the left is positive but the swipe from the right edge is negative, hence the call to abs to convert it for right swipes.
- Examines the gestureRecognizer state and acts based on the status of the gesture
- .began: If the gesture is just starting, checks if it's from the right or left. If right, it calls setupAndPresentNextViewController() since a right swipe means the present function should be called and we'll need to instantiate the next ViewController. If a left swipe, we'll dismiss the current UIViewController. In both cases, we set the interactionInProgress to true. Note: The setupAndPresentNextViewController() method is detailed further below.
- .changed: Call the UIPercentDrivenInteractiveTransition method update() with the current progress to tell UIKit the current completion percentage of the transition. Set the class flag shouldCompleteTransition to true
- .canceled: If the edge pan gesture is halted unexpectedly this code is called to reset the flag. It then calls the UIPercentDrivenInteractiveTransition cancel() method to update UIKit that the transition is cancelled.
- .ended: This state will always be invoked by the gestureRecognizer.
- Depending on the value of shouldCompleteTransition, which is set to true or false based on progress in the .changed case, either call finish() from UIPercentDrivenInteractiveTransition to tell UIKit to complete the transition or cancel() to tell UIKit to revert the transition to the original state.
Finally, complete SlideInteractionController by implementing the setupAndPresentNextViewController() method. Add the following code:
private func setupAndPresentNextViewController(){
interactionInProgress = true
if let vc = viewController as? FirstViewController{
let secondVC = (UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "secondviewcontroller") as? SecondViewController)!
secondVC.transitioningDelegate = vc
viewController.present(secondVC, animated: true, completion:nil)
}else if let vc = viewController as? SecondViewController{
vc.slideInteractionController?.interactionInProgress = true
let thirdVC = (UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "thirdviewcontroller") as? ThirdViewController)!
thirdVC.transitioningDelegate = vc
viewController.present(thirdVC, animated: true, completion:nil)
}
}
The class variable, viewController, is a reference to the UIViewController that owns the instance of SlideInteractionController.
In order to invoke the presentation of the next UIViewController check the type of the currently presented one. That's either FirstViewController or SecondViewController. Note that ThirdViewController does not need to be cast because the transition does not go any further to other UIViewControllers beyond the second one.
Make sure to set the local interactionInProgress variable to true since the manual transition to the next UIViewController is impending.
The next parts of the code:
- Instantiate an instance of the next UIViewController from the Storyboard.
- Set that newly created UIViewController transitioningDelegate to the currently presented UIViewController.
- Call the present(viewControllerToPresent:animated:completion) method to tell UIKit we're ready to start the transition.
UIViewControllerTransitioningDelegate
Only the first two UIViewControllers need to implement the UIViewControllerTransitioningDelegate. The transition is only back from the third UIViewController and then the UIViewControllerTransitioningDelegate methods from SecondViewController are called instead.
The protocol code from FirstViewController is shown below. The code for SecondViewController is identical except for the @IBAction and prepare(segue:sender). Reference the code from the repo on bitbucket.org for the SecondViewController and ThirdViewController code.
Add the following extension code to your FirstViewController class:
// MARK: UIViewControllerTransitioningDelegate
extension FirstViewController:UIViewControllerTransitioningDelegate{
func animationController(forPresented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?{
return SlidePresentAnimationController(originFrame: self.view.frame, interactionController: self.slideInteractionController)
}
}
This is an implementation of the UIViewControllerTransitioningDelegate method call that returns the present animator to UIKit. It includes the SlideInteractionController instance of the class used for manual interaction when creating the SlidePresentAnimationController.
Next, add the following method to that extension:
// Return the Present Animator
func animationController(forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
var t_slideInteractionController: SlideInteractionController? = nil
// Only set the interaction controller if the incoming "dismissed" ViewController is SecondViewController
if let dismissedVC = dismissed as? SecondViewController {
t_slideInteractionController = dismissedVC.slideInteractionController
}
return SlideDismissAnimationController(destinationFrame: self.view.frame,
interactionController: t_slideInteractionController)
}
This is an implementation of the UIViewControllerTransitioningDelegate method call returning the dismiss animator to UIKit when it requests one. There is a cast of the dismissed UIViewController to check if it is an instance of SecondViewController before pulling its slideInteractionController and using it to instantiate the SlideDismissAnimationController.
Finally, add the following code to the extension:
// Return the Dismiss Animator
func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning)
-> UIViewControllerInteractiveTransitioning? {
guard let animator = animator as? SlideDismissAnimationController,
let interactionController = animator.interactionController,
interactionController.interactionInProgress
else {
return nil
}
return interactionController
}
func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
// The SlideDismissAnimationController has our custom SlideInteractionController
// set when it's initialized. Get that and return it to UIKit so it can act
// upon the user interaction when UIKit calls the delegate functions.
guard let animator = animator as? SlidePresentAnimationController,
let interactionController = animator.interactionController,
interactionController.interactionInProgress
else {
return nil
}
return interactionController
}
Each of these method calls return the instance of the SlideInteractionController for presenting and dismissing used to initialize the animator in the previous method calls. In detail, both method calls guard to ensure the following:
- Received the correct animator SlidePresentAnimationController or SlideDismissAnimationController
- Check that the animator has an interactionController
- Check that that interactionController is currently set to interactionInProgress.
If any of those checks fails, both methods will return nil which will prevent manual interaction with the transition animation, otherwise the manual interaction can proceed accordingly.
Now add the the same UIViewControllerTransitioningDelegate methods to SecondViewController. Change one item, the animationController(forDismissed:) method to cast the dismissed UIViewController to ThirdViewController.
Final Thoughts
Lots of detailed and fairly complex information to digest here. Correctly following the steps detailed above should have resulted in an app with a unique animated transition. If not, download the provided project code and run it. For tweaking purposes, go into the SlideInteractionController and adjust the timing, animation duration, progress calculations, and more, to see the effect on the animation.
The animations in this tutorial aren't the most mind-blowing but they are smooth and beautiful. Get comfortable with all the components described here and then apply imagination to create something far greater than what's provided by the code here.
Here are some key links discussed in this tutorial for reference: