UIKit12 min readJul 4, 2026

Mastering Custom View Controller Transitions in UIKit

Custom view controller transitions in UIKit allow you to define unique and engaging animations when presenting or dismissing view controllers. This guide will walk you through the core concepts and practical implementation steps to create your own bespoke transitions, adding a professional touch to your iOS applications.

The Power of Custom Transitions

Standard UIKit transitions like push, pop, present, and dismiss are functional, but they can often feel generic. Custom view controller transitions open up a world of creative possibilities, allowing you to design animations that perfectly complement your app's brand and user experience. Whether you're building a unique onboarding flow, a custom photo viewer, or a game with distinct scene changes, custom transitions can make your app stand out.

Why Custom Transitions Matter:

  • Enhanced User Experience: Smooth, intuitive animations guide users and make interactions feel more natural and responsive.
  • Brand Identity: Imbue your app with a unique visual language, reinforcing your brand's aesthetics.
  • Storytelling: Transitions can tell a story, leading the user's eye and highlighting important elements as they navigate your app.
  • Performance: While standard transitions are optimized, custom transitions give you granular control, allowing you to fine-tune performance for specific use cases.

UIKit provides a robust framework for managing these transitions, primarily relying on protocols that you conform to. This article will focus on non-interactive custom transitions first, then touch upon interactive dismissal.

Understanding the Core Protocols

At the heart of custom view controller transitions are three key protocols:

  1. UIViewControllerAnimatedTransitioning: This protocol defines the animator object responsible for performing the actual animation. It specifies how long the animation should last and how to animate the views during the transition.
  2. UIViewControllerTransitioningDelegate: When presenting a view controller modally, this delegate is responsible for providing the animator objects for presentation, dismissal, and optionally, interaction controllers. It acts as the central coordinator.
  3. UINavigationControllerDelegate: For transitions within a UINavigationController (push/pop), this delegate provides the animator object. It's the navigation controller's counterpart to UIViewControllerTransitioningDelegate.

Let's break down UIViewControllerAnimatedTransitioning first, as it's fundamental to both modal and navigation-based transitions. An object conforming to this protocol is often referred to as an "animator object" or "transition animator."

Creating a Custom Transition Animator

Your custom animator object will conform to UIViewControllerAnimatedTransitioning. This object will be responsible for orchestrating the visual changes that occur during your transition. Let's create a simple "fade in" effect for a presentation.

First, define a custom animator class. This class will implement two required methods:

  • transitionDuration(using:): Returns the total duration of your animation.
  • animateTransition(using:): This is where the magic happens. You'll manipulate view hierarchies and animate changes within this method.
swift
import UIKit

class FadeAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    let isPresenting: Bool

    init(isPresenting: Bool) {
        self.isPresenting = isPresenting
        super.init()
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.7 // Our animation will last 0.7 seconds
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // 1. Get references to the view controllers and their views involved in the transition
        let containerView = transitionContext.containerView
        guard let fromVC = transitionContext.viewController(forKey: .from),
              let toVC = transitionContext.viewController(forKey: .to) else { return }

        let fromView = fromVC.view!
        let toView = toVC.view!

        // 2. Add the destination view to the container view
        // For presentation, 'toView' will appear; for dismissal, 'fromView' will disappear.
        if isPresenting {
            containerView.addSubview(toView)
            toView.alpha = 0.0 // Start invisible
        } else {
            // For dismissal, 'toView' (the presenting view controller's view) is already in container
            // 'fromView' (the presented view controller's view) will be removed
        }

        // Get the final frame for 'toView' (important for iOS 13+ where views might not start at full size)
        let finalFrameForToView = transitionContext.finalFrame(for: toVC)
        toView.frame = finalFrameForToView // Ensure 'toView' has its final position

        // 3. Perform the UIView animation
        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            if self.isPresenting {
                toView.alpha = 1.0 // Fade in
            } else {
                fromView.alpha = 0.0 // Fade out
            }
        }) { _ in
            // 4. Complete the transition! VERY IMPORTANT!
            // This tells UIKit that your animation is done and it can clean up.
            // If you don't call this, your app will be stuck.
            let didComplete = !transitionContext.transitionWasCancelled
            if !didComplete && self.isPresenting { // If cancelled during presentation, remove toView
                toView.removeFromSuperview()
            }
            transitionContext.completeTransition(didComplete)
        }
    }
}

Compatibility Notes: This pattern is robust across all modern iOS versions (iOS 9+ up to the latest). Pay close attention to transitionContext.finalFrame(for:) for toVC, especially for iOS 13+ and modal presentations, as the view's initial frame might not always be what you expect without explicit setup.

swift
import UIKit

class FadeAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    let isPresenting: Bool

    init(isPresenting: Bool) {
        self.isPresenting = isPresenting
        super.init()
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.7 // Our animation will last 0.7 seconds
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // 1. Get references to the view controllers and their views involved in the transition
        let containerView = transitionContext.containerView
        guard let fromVC = transitionContext.viewController(forKey: .from),
              let toVC = transitionContext.viewController(forKey: .to) else { return }

        let fromView = fromVC.view!
        let toView = toVC.view!

        // 2. Add the destination view to the container view
        // For presentation, 'toView' will appear; for dismissal, 'fromView' will disappear.
        if isPresenting {
            containerView.addSubview(toView)
            toView.alpha = 0.0 // Start invisible
        } else {
            // For dismissal, 'toView' (the presenting view controller's view) is already in container
            // 'fromView' (the presented view controller's view) will be removed
        }

        // Get the final frame for 'toView' (important for iOS 13+ where views might not start at full size)
        let finalFrameForToView = transitionContext.finalFrame(for: toVC)
        toView.frame = finalFrameForToView // Ensure 'toView' has its final position

        // 3. Perform the UIView animation
        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            if self.isPresenting {
                toView.alpha = 1.0 // Fade in
            } else {
                fromView.alpha = 0.0 // Fade out
            }
        }) { _ in
            // 4. Complete the transition! VERY IMPORTANT!
            // This tells UIKit that your animation is done and it can clean up.
            // If you don't call this, your app will be stuck.
            let didComplete = !transitionContext.transitionWasCancelled
            if !didComplete && self.isPresenting { // If cancelled during presentation, remove toView
                toView.removeFromSuperview()
            }
            transitionContext.completeTransition(didComplete)
        }
    }
}

Implementing a Custom Modal Presentation Transition

Now that we have our FadeAnimator, let's integrate it into a modal presentation. For modal presentations, you'll use UIViewControllerTransitioningDelegate. This delegate is set on the presented view controller.

Let's assume you have FirstViewController (presenting) and SecondViewController (to be presented).

swift
import UIKit

// FirstViewController.swift
class FirstViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBlue

        let presentButton = UIButton(type: .system)
        presentButton.setTitle("Present Second VC", for: .normal)
        presentButton.setTitleColor(.white, for: .normal)
        presentButton.addTarget(self, action: #selector(presentSecondVC), for: .touchUpInside)
        presentButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(presentButton)

        NSLayoutConstraint.activate([
            presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func presentSecondVC() {
        let secondVC = SecondViewController()
        // Set the modal presentation style to .custom
        // This tells UIKit to look for a custom transitioning delegate.
        secondVC.modalPresentationStyle = .custom

        // Assign self (or another object) as the transitioning delegate.
        // It is CRUCIAL to hold a strong reference to the delegate.
        // If not, it can be deallocated before the transition starts.
        secondVC.transitioningDelegate = self

        present(secondVC, animated: true, completion: nil)
    }
}

// Conform FirstViewController to UIViewControllerTransitioningDelegate
extension FirstViewController: UIViewControllerTransitioningDelegate {

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

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

// SecondViewController.swift
class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemRed

        let dismissButton = UIButton(type: .system)
        dismissButton.setTitle("Dismiss", for: .normal)
        dismissButton.setTitleColor(.white, for: .normal)
        dismissButton.addTarget(self, action: #selector(dismissVC), for: .touchUpInside)
        dismissButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(dismissButton)

        NSLayoutConstraint.activate([
            dismissButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            dismissButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func dismissVC() {
        dismiss(animated: true, completion: nil)
    }
}

Important Considerations:

  • modalPresentationStyle = .custom: This is essential. Without it, UIKit will use its default modal presentation style and ignore your transitioningDelegate.
  • Strong Reference to Delegate: The transitioningDelegate property is weak. If you set it to a local object, it will be immediately deallocated. In the example above, FirstViewController sets itself as the delegate for secondVC, ensuring it has a strong reference for the duration of the transition. For more complex scenarios, you might use a dedicated delegate object and hold a strong reference to it on the presenting view controller or in your app's flow coordinator.

Compatibility Notes: This delegate pattern works consistently across all recent iOS versions.

swift
import UIKit

// FirstViewController.swift
class FirstViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBlue

        let presentButton = UIButton(type: .system)
        presentButton.setTitle("Present Second VC", for: .normal)
        presentButton.setTitleColor(.white, for: .normal)
        presentButton.addTarget(self, action: #selector(presentSecondVC), for: .touchUpInside)
        presentButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(presentButton)

        NSLayoutConstraint.activate([
            presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func presentSecondVC() {
        let secondVC = SecondViewController()
        // Set the modal presentation style to .custom
        // This tells UIKit to look for a custom transitioning delegate.
        secondVC.modalPresentationStyle = .custom

        // Assign self (or another object) as the transitioning delegate.
        // It is CRUCIAL to hold a strong reference to the delegate.
        // If not, it can be deallocated before the transition starts.
        secondVC.transitioningDelegate = self

        present(secondVC, animated: true, completion: nil)
    }
}

// Conform FirstViewController to UIViewControllerTransitioningDelegate
extension FirstViewController: UIViewControllerTransitioningDelegate {

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

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

// SecondViewController.swift
class SecondViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemRed

        let dismissButton = UIButton(type: .system)
        dismissButton.setTitle("Dismiss", for: .normal)
        dismissButton.setTitleColor(.white, for: .normal)
        dismissButton.addTarget(self, action: #selector(dismissVC), for: .touchUpInside)
        dismissButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(dismissButton)

        NSLayoutConstraint.activate([
            dismissButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            dismissButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func dismissVC() {
        dismiss(animated: true, completion: nil)
    }
}

Custom Navigation Controller Transitions

For UINavigationController push and pop operations, you'll use UINavigationControllerDelegate. The navigation controller itself will typically be its own delegate or you can set a separate object as its delegate.

swift
import UIKit

// Assuming FadeAnimator is defined as in the previous section

class RootNavigationController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self // Set self as the navigation controller's delegate

        // Set initial view controller
        let firstVC = FirstNavViewController()
        setViewControllers([firstVC], animated: false)
    }
}

extension RootNavigationController: UINavigationControllerDelegate {

    // This method is called when a push or pop transition is about to occur.
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        switch operation {
        case .push:
            return FadeAnimator(isPresenting: true) // Use our fade animator for push
        case .pop:
            return FadeAnimator(isPresenting: false) // Use our fade animator for pop
        case .none:
            return nil // No custom animation needed for other operations
        @unknown default:
            return nil
        }
    }
}

// FirstNavViewController.swift - A simple VC to push another
class FirstNavViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemCyan
        title = "First Nav VC"

        let pushButton = UIButton(type: .system)
        pushButton.setTitle("Push Second Nav VC", for: .normal)
        pushButton.setTitleColor(.white, for: .normal)
        pushButton.addTarget(self, action: #selector(pushSecondNavVC), for: .touchUpInside)
        pushButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(pushButton)

        NSLayoutConstraint.activate([
            pushButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            pushButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func pushSecondNavVC() {
        let secondVC = SecondNavViewController()
        navigationController?.pushViewController(secondVC, animated: true)
    }
}

// SecondNavViewController.swift - A simple VC to be pushed
class SecondNavViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemPink
        title = "Second Nav VC"

        let popButton = UIButton(type: .system)
        popButton.setTitle("Pop", for: .normal)
        popButton.setTitleColor(.white, for: .normal)
        popButton.addTarget(self, action: #selector(popVC), for: .touchUpInside)
        popButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(popButton)

        NSLayoutConstraint.activate([
            popButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            popButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func popVC() {
        navigationController?.popViewController(animated: true)
    }
}

Key Difference: Unlike modal presentations where transitioningDelegate is set on the presented UIViewController, UINavigationControllerDelegate is set on the UINavigationController itself. This single delegate handles all push and pop operations for that navigation stack.

Compatibility Notes: This pattern has been stable and effective since iOS 7.

swift
import UIKit

// Assuming FadeAnimator is defined as in the previous section

class RootNavigationController: UINavigationController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self // Set self as the navigation controller's delegate

        // Set initial view controller
        let firstVC = FirstNavViewController()
        setViewControllers([firstVC], animated: false)
    }
}

extension RootNavigationController: UINavigationControllerDelegate {

    // This method is called when a push or pop transition is about to occur.
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

        switch operation {
        case .push:
            return FadeAnimator(isPresenting: true) // Use our fade animator for push
        case .pop:
            return FadeAnimator(isPresenting: false) // Use our fade animator for pop
        case .none:
            return nil // No custom animation needed for other operations
        @unknown default:
            return nil
        }
    }
}

// FirstNavViewController.swift - A simple VC to push another
class FirstNavViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemCyan
        title = "First Nav VC"

        let pushButton = UIButton(type: .system)
        pushButton.setTitle("Push Second Nav VC", for: .normal)
        pushButton.setTitleColor(.white, for: .normal)
        pushButton.addTarget(self, action: #selector(pushSecondNavVC), for: .touchUpInside)
        pushButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(pushButton)

        NSLayoutConstraint.activate([
            pushButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            pushButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func pushSecondNavVC() {
        let secondVC = SecondNavViewController()
        navigationController?.pushViewController(secondVC, animated: true)
    }
}

// SecondNavViewController.swift - A simple VC to be pushed
class SecondNavViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemPink
        title = "Second Nav VC"

        let popButton = UIButton(type: .system)
        popButton.setTitle("Pop", for: .normal)
        popButton.setTitleColor(.white, for: .normal)
        popButton.addTarget(self, action: #selector(popVC), for: .touchUpInside)
        popButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(popButton)

        NSLayoutConstraint.activate([
            popButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            popButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    @objc func popVC() {
        navigationController?.popViewController(animated: true)
    }
}

Beyond Basics: Interactive Transitions

While non-interactive transitions are great for many scenarios, interactive transitions allow users to control the animation, such as swiping to dismiss a view controller. This involves another protocol: UIViewControllerInteractiveTransitioning.

You typically combine an UIPercentDrivenInteractiveTransition with a gesture recognizer (UIScreenEdgePanGestureRecognizer for dismissal from an edge, or UIPanGestureRecognizer for full-screen dragging). The UIViewControllerTransitioningDelegate or UINavigationControllerDelegate then provides an interactionControllerForPresentation or interactionControllerForDismissal based on whether an interactive gesture is active.

Implementing interactive transitions can be more complex, requiring careful management of gesture states (.began, .changed, .ended, .cancelled) to trigger update(_:), finish(), or cancel() on your UIPercentDrivenInteractiveTransition instance. When transitionWasCancelled is true in your animator, your cleanup logic needs to account for this.

For a deeper dive into interactive transitions, consider exploring tutorials specifically focused on UIPercentDrivenInteractiveTransition and gesture handling, as integrating them robustly requires more boilerplate code than can be covered in a basic introduction.

Tips for Great Custom Transitions

Creating compelling custom transitions goes beyond just making things move. Here are some pro tips:

  • Keep it brief: Animations should be noticeable but not annoy the user. Aim for 0.3 to 0.7 seconds for most transitions.
  • Maintain Context: Animate similar elements together. For example, if an image on screen A expands to become the main image on screen B, animate that transformation.
  • Performance: Avoid heavy computations or image processing during animation. UIKit's UIView.animate is highly optimized. If you need more control or complex physics, explore UIDynamicAnimator or CADisplayLink.
  • Interruption Handling: Consider how your animation behaves if the user taps furiously or triggers another interaction. Ensure completeTransition(_:) is always called.
  • Reusability: Design your animator objects to be reusable, perhaps by passing configuration options (like animation direction or specific views to animate) during initialization.

Transitions are just setting animated

Becoming a stronger iOS Engineer

THE MYTH or PROBLEM: Transitions are just setting animated:true

Many developers think `present(..., animated: true)` is the extent of UIKit transitions. While functional, this limits UX creativity and app personality. Attempting complex custom animations without the `UIViewControllerAnimatedTransitioning` protocol often leads to broken view hierarchies and inconsistent states.

swift
present(MyViewController(), animated: true, completion: nil)

TASK HIERARCHY: Custom Transition Process

UIKit orchestrates view controller transitions through a series of delegate callbacks and animator objects, allowing you to inject custom animation logic at specific points in the transition lifecycle.

UI Transition System
UIViewControllerTransitioningDelegate (Modal)
UINavigationControllerDelegate (Nav Stack)
UIViewControllerAnimatedTransitioning (Animator)
1

1. Trigger Transition

Call `present(:animated:completion:)` or `pushViewController(:animated:)` with `animated: true`. For modal, `modalPresentationStyle = .custom` must be set.

2

2. Get Delegate

UIKit asks the `transitioningDelegate` (for modal) or `navigationController.delegate` (for nav stack) for an animator object.

3

3. Get Animator

The delegate returns an object conforming to `UIViewControllerAnimatedTransitioning` (your custom animator).

4

4. Setup Container View

UIKit provides a `containerView` to the animator. The animator adds 'to' view and potentially removes 'from' view.

5

5. Animate

The animator performs custom UIView animations on the 'to' and 'from' views within the `containerView`.

6

6. Complete Transition

The animator MUST call `transitionContext.completeTransition(didComplete)` to signal completion and allow UIKit to clean up.

Visualized execution hierarchy.

Powerful Guarantees

Coordinated Cleanup

UIKit ensures proper view hierarchy management once `completeTransition` is called, preventing orphan views or misplaced view controllers.

Flexibility

Full control over view positions, transforms, and alpha allows for any animation desired.

Performance Handling

Optimized animation context passed via `UIViewControllerContextTransitioning` for smooth interaction.

REAL PRODUCTION EXAMPLE: Photos App Image Zoom Transition

When you tap an image in the Photos app's grid view, it expands and zooms into its full-screen detail view. When dismissed, it shrinks back to its original position in the grid. This is a classic custom transition, often using `Hero` or a similar framework built on these UIKit primitives, or a hand-rolled animated transition for shared elements.

Impact / Results
Seamless user experience
Visual continuity between screens
Professional app feel
THE FIX or SOLUTION: Shared Element Transition
swift
class ImageZoomAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    // ... (omitting transitionDuration for brevity)
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let containerView = transitionContext.containerView
        guard let fromVC = transitionContext.viewController(forKey: .from),
              let toVC = transitionContext.viewController(forKey: .to),
              // Assume both VCs provide a reference to the 'starting' and 'ending' image view
              let startingImageView = (isPresenting ? (fromVC as? SourceVC)?.selectedImageView : (fromVC as? DestinationVC)?.imageView),
              let endingImageView = (isPresenting ? (toVC as? DestinationVC)?.imageView : (toVC as? SourceVC)?.selectedImageView)
        else { return }

        let snapshot = startingImageView.snapshotView(afterScreenUpdates: false)!
        snapshot.frame = containerView.convert(startingImageView.frame, from: startingImageView.superview)
        
        let toView = toVC.view!
        toView.alpha = 0 // Hide destination VC initially
        containerView.addSubview(toView)
        containerView.addSubview(snapshot)

        endingImageView.alpha = 0 // Hide the destination image itself

        UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
            // Animate snapshot to final position and size
            snapshot.frame = containerView.convert(endingImageView.frame, from: endingImageView.superview)
            toView.alpha = 1 // Fade in destination VC
        }) { _ in
            snapshot.removeFromSuperview()
            endingImageView.alpha = 1 // Show the real destination image
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    }
}

INTERVIEW PERSPECTIVE

Common Question

Describe the core components and flow of a custom view controller transition in UIKit.

Strong Answer

A strong answer would involve explaining the three key protocols (`UIViewControllerAnimatedTransitioning`, `UIViewControllerTransitioningDelegate`, `UINavigationControllerDelegate`) and their roles. It should detail how an animator object is provided, how it uses the `transitionContext` to get `fromVC`, `toVC`, and the `containerView`, and emphasize the critical step of calling `completeTransition` to signal animation completion to UIKit. Mentioning `modalPresentationStyle = .custom` for modal transitions is also a plus.

Interviewers Expect you to understand:
  • Protocol understanding
  • Role of animator and delegates
  • Importance of `completeTransition`
  • Conceptual flow (`containerView`, `fromView`, `toView` manipulation)
KEY TAKEAWAY

Custom view controller transitions in UIKit leverage protocols to provide granular control over view animation during presentation and dismissal, enabling highly engaging and branded user experiences. Always call `completeTransition()` and manage strong references to your delegates.

Frequently Asked Questions

What is the primary difference between `UIViewControllerTransitioningDelegate` and `UINavigationControllerDelegate` in custom transitions?
The `UIViewControllerTransitioningDelegate` is used for custom *modal* presentations and dismissals. You set this delegate on the *presented* view controller. The `UINavigationControllerDelegate` is used for custom push and pop operations within a `UINavigationController` stack. You set this delegate on the `UINavigationController` itself.
Why is `modalPresentationStyle = .custom` necessary for custom modal transitions?
Setting `modalPresentationStyle` to `.custom` explicitly tells UIKit that you intend to provide your own custom presentation and dismissal logic via the `transitioningDelegate`. If you don't set this, UIKit will use its default presentation styles (like `.automatic` or `.fullScreen`) and ignore your custom delegate.
What happens if I forget to call `transitionContext.completeTransition(didComplete)`?
If you fail to call `completeTransition(_:)`, UIKit will not know that your animation has finished. This will leave your app in an inconsistent state, views may not be properly added or removed, gestures might stop working, and subsequent transitions may fail or behave incorrectly. It's a critical step for proper cleanup and state management.
How do I ensure my `transitioningDelegate` is not deallocated prematurely?
The `transitioningDelegate` property is declared as `weak`. You must maintain a strong reference to your delegate object elsewhere. A common approach is for the presenting view controller to act as its own delegate (if appropriate), or to store the delegate object as a strong property on the presenting view controller or a dedicated coordinator object.
Can I use custom transitions in SwiftUI?
While SwiftUI has its own powerful `matchedGeometryEffect` and `transition` modifiers for view-level animations, for full view controller transitions (presenting or dismissing a `UIViewController` embedded in SwiftUI), you would still fall back to UIKit's `UIViewControllerRepresentable` and apply these UIKit custom transition techniques within the `UIViewControllerRepresentable`'s coordinator or the presented `UIViewController` itself.
#UIKit#Transitions#Animation#iOS Development#UIViewController