How to Create Custom View Controller Transitions in iOS: Animations Guide

Create custom view controller transitions in iOS using UIViewControllerAnimatedTransitioning with UIViewControllerTransitioningDelegate. UIViewControllerAnimatedTransitioning defines the animation duration and the animation code. UIViewControllerTransitioningDelegate provides the animator object when a presentation or dismissal occurs. UIKit calls these protocols automatically when you set a transitioningDelegate on the presented view controller.

What Is the View Controller Transitioning API?

The iOS View Controller Transitioning API has three core protocols. UIViewControllerAnimatedTransitioning contains the animation logic. UIViewControllerTransitioningDelegate vends animator objects to the system. UIViewControllerContextTransitioning provides access to the from-view, to-view, and container view during the transition.

The flow is straightforward. Set transitioningDelegate on the presented view controller. When presentation begins, UIKit asks the delegate for an animator. The animator's animateTransition method runs with a transition context. The animator performs the animation and calls completeTransition when finished.

How to Build a Slide-Down Transition

Create an animator class that adopts both UIViewControllerAnimatedTransitioning and UIViewControllerTransitioningDelegate. Use an isPresenting flag to control forward and reverse animation.

class SlideDownAnimator: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
    let duration: TimeInterval = 0.5
    var isPresenting = false

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.view(forKey: .from),
              let toView = transitionContext.view(forKey: .to)
        else { return }

        let container = transitionContext.containerView
        let containerHeight = container.frame.height

        if isPresenting {
            toView.transform = CGAffineTransform(translationX: 0, y: -containerHeight)
        }

        container.addSubview(fromView)
        container.addSubview(toView)

        UIView.animate(
            withDuration: duration,
            delay: 0,
            usingSpringWithDamping: 0.8,
            initialSpringVelocity: 0.8,
            animations: {
                if self.isPresenting {
                    fromView.transform = CGAffineTransform(translationX: 0, y: containerHeight)
                    fromView.alpha = 0.5
                    toView.transform = .identity
                } else {
                    fromView.transform = CGAffineTransform(translationX: 0, y: -containerHeight)
                    toView.transform = .identity
                }
            },
            completion: { _ in
                transitionContext.completeTransition(true)
            }
        )
    }

    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresenting = true
        return self
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        isPresenting = false
        return self
    }
}

Wire it up by setting the transitioningDelegate.

let slideDown = SlideDownAnimator()
destinationVC.transitioningDelegate = slideDown
destinationVC.modalPresentationStyle = .fullScreen

The modalPresentationStyle must be .fullScreen on iOS 13 and later. The default card-style presentation interferes with custom full-screen transitions.

How to Build a Slide-Right Transition

A slide-right transition moves the new view in from the left. The key difference is view ordering in the container. When presenting, add fromView first, then toView on top. When dismissing, reverse the order.

class SlideRightAnimator: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
    let duration: TimeInterval = 0.4
    var isPresenting = false

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.view(forKey: .from),
              let toView = transitionContext.view(forKey: .to)
        else { return }

        let container = transitionContext.containerView
        let offScreenLeft = CGAffineTransform(translationX: -container.frame.width, y: 0)

        if isPresenting {
            toView.transform = offScreenLeft
            container.addSubview(fromView)
            container.addSubview(toView)
        } else {
            container.addSubview(toView)
            container.addSubview(fromView)
        }

        UIView.animate(withDuration: duration, animations: {
            if self.isPresenting {
                toView.transform = .identity
            } else {
                fromView.transform = offScreenLeft
            }
        }, completion: { _ in
            transitionContext.completeTransition(true)
        })
    }
}

The view ordering rule ensures the dismissing view animates out while the returning view stays visible underneath.

How to Build a Pop Transition

A pop transition scales the new view from nothing while the current view shrinks and dims.

class PopAnimator: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
    let duration: TimeInterval = 0.4
    var isPresenting = false

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.view(forKey: .from),
              let toView = transitionContext.view(forKey: .to)
        else { return }

        let container = transitionContext.containerView

        if isPresenting {
            toView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
            container.addSubview(fromView)
            container.addSubview(toView)
        } else {
            container.addSubview(toView)
            container.addSubview(fromView)
        }

        UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, animations: {
            if self.isPresenting {
                fromView.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
                fromView.alpha = 0.7
                toView.transform = .identity
            } else {
                fromView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
                fromView.alpha = 0
                toView.transform = .identity
                toView.alpha = 1
            }
        }, completion: { _ in
            transitionContext.completeTransition(true)
        })
    }
}

This creates an app-store-like card animation where new content pops into view and previous content recedes.

How to Build a Rotate Transition

A rotate transition pivots views around a custom anchor point. Change the anchorPoint to rotate around a corner instead of the default center.

class RotateAnimator: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
    let duration: TimeInterval = 0.6
    var isPresenting = false

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let fromView = transitionContext.view(forKey: .from),
              let toView = transitionContext.view(forKey: .to)
        else { return }

        let container = transitionContext.containerView
        let rotateAngle = CGFloat(-90 * Double.pi / 180)

        if isPresenting {
            toView.layer.anchorPoint = CGPoint(x: 0, y: 0)
            toView.layer.position = .zero
            toView.transform = CGAffineTransform(rotationAngle: rotateAngle)
            container.addSubview(fromView)
            container.addSubview(toView)
        } else {
            fromView.layer.anchorPoint = CGPoint(x: 0, y: 0)
            fromView.layer.position = .zero
            container.addSubview(toView)
            container.addSubview(fromView)
        }

        UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, animations: {
            if self.isPresenting {
                toView.transform = .identity
            } else {
                fromView.transform = CGAffineTransform(rotationAngle: rotateAngle)
            }
        }, completion: { _ in
            transitionContext.completeTransition(true)
        })
    }
}

Changing anchorPoint without adjusting position causes the view to jump. Always set position to match the new anchor point.

Practical Tips for Custom Transitions

Always call completeTransition(true) in the animation completion block. The system waits for this call and does not finish the transition without it.

Use spring animations for natural motion. Pure linear animations feel robotic. Spring damping of 0.7-0.8 with velocity of 0.5-0.8 produces professional results.

Manage view hierarchy carefully. Add fromView first and toView on top when presenting. Reverse the order when dismissing so the returning view is underneath.

Set modalPresentationStyle to .fullScreen. iOS 13 default card presentation interferes with custom transitions that manipulate the entire screen.

Handle both directions in one animator class. An isPresenting flag keeps forward and reverse animations symmetric and easier to maintain.

Summary

This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.

Frequently Asked Questions

What iOS version is required? This tutorial targets iOS 13. Most of the APIs shown work on earlier versions as well, but some newer APIs may require the version specified in the article.

What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.

Can I use this with SwiftUI instead of UIKit? The article demonstrates the UIKit approach. SwiftUI has its own APIs for similar functionality. You can adapt the concepts, but the implementation details differ between the two frameworks.

All articles