iOS
Swift
ViewController
Animation
App Development

How to present view controller from right to left in iOS using Swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

UIKit's default modal animation slides a view controller up from the bottom, but many apps want a horizontal transition that feels closer to a navigation push. To present from right to left, the standard solution is a custom modal transition using a transitioning delegate and animator objects.

Understand the Pieces of a Custom Transition

A custom modal presentation has three moving parts:

  • the presented view controller
  • a transitioning delegate
  • animator objects for presentation and dismissal

The delegate acts as a factory for animation behavior, while the animator performs the actual frame changes. Keeping these responsibilities separate makes the transition reusable across screens.

Create a Right-to-Left Animator

The animator below handles both presentation and dismissal. Presentation starts off-screen to the right. Dismissal moves the current view back to the right.

swift
1import UIKit
2
3final class HorizontalSlideAnimator: NSObject, UIViewControllerAnimatedTransitioning {
4    enum Mode {
5        case present
6        case dismiss
7    }
8
9    private let mode: Mode
10    private let duration: TimeInterval
11
12    init(mode: Mode, duration: TimeInterval = 0.35) {
13        self.mode = mode
14        self.duration = duration
15    }
16
17    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
18        duration
19    }
20
21    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
22        let container = transitionContext.containerView
23
24        switch mode {
25        case .present:
26            guard
27                let toVC = transitionContext.viewController(forKey: .to),
28                let toView = transitionContext.view(forKey: .to)
29            else {
30                transitionContext.completeTransition(false)
31                return
32            }
33
34            let finalFrame = transitionContext.finalFrame(for: toVC)
35            toView.frame = finalFrame.offsetBy(dx: finalFrame.width, dy: 0)
36            container.addSubview(toView)
37
38            UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseOut]) {
39                toView.frame = finalFrame
40            } completion: { finished in
41                transitionContext.completeTransition(finished)
42            }
43
44        case .dismiss:
45            guard let fromView = transitionContext.view(forKey: .from) else {
46                transitionContext.completeTransition(false)
47                return
48            }
49
50            let targetFrame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0)
51
52            UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseIn]) {
53                fromView.frame = targetFrame
54            } completion: { finished in
55                transitionContext.completeTransition(finished)
56            }
57        }
58    }
59}

This is enough for a basic reusable horizontal modal transition.

Provide the Animator Through a Transitioning Delegate

The transitioning delegate returns one animator for presentation and another for dismissal.

swift
1import UIKit
2
3final class HorizontalTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
4    private let presentAnimator = HorizontalSlideAnimator(mode: .present)
5    private let dismissAnimator = HorizontalSlideAnimator(mode: .dismiss)
6
7    func animationController(
8        forPresented presented: UIViewController,
9        presenting: UIViewController,
10        source: UIViewController
11    ) -> UIViewControllerAnimatedTransitioning? {
12        presentAnimator
13    }
14
15    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
16        dismissAnimator
17    }
18}

This keeps transition setup small in the presenting controller and makes the animation logic easy to share.

Present the Destination Controller Correctly

To use the custom transition, set modalPresentationStyle to .custom, assign the transitioning delegate, and then call present.

swift
1import UIKit
2
3final class HomeViewController: UIViewController {
4    private let horizontalTransitionDelegate = HorizontalTransitionDelegate()
5
6    @IBAction func showDetailsTapped(_ sender: UIButton) {
7        let details = DetailsViewController()
8        details.modalPresentationStyle = .custom
9        details.transitioningDelegate = horizontalTransitionDelegate
10        present(details, animated: true)
11    }
12}
13
14final class DetailsViewController: UIViewController {
15    @IBAction func closeTapped(_ sender: UIButton) {
16        dismiss(animated: true)
17    }
18}

The presenting controller must keep a strong reference to the transitioning delegate. If it is created as a temporary local variable, the system may fall back to the default transition.

When This Is Better Than a Navigation Push

If the screen is conceptually a modal task but you still want horizontal motion, this approach makes sense. For example:

  • settings or filter screens presented over the current flow
  • onboarding steps shown modally
  • temporary detail flows that should dismiss back to the origin

If the destination is actually part of a standard hierarchical navigation stack, a UINavigationController push is usually the simpler and more native choice.

Optional: Add Interactive Dismissal Later

If the design calls for swipe-to-dismiss behavior, you can extend the transition with UIPercentDrivenInteractiveTransition. That is useful, but it also adds gesture coordination and more edge cases. It is worth adding only when the product flow specifically benefits from interactive dismissal.

Common Pitfalls

The most common mistake is forgetting to set modalPresentationStyle = .custom. Without that, UIKit ignores the custom animator and uses the default modal transition.

Another issue is creating the transitioning delegate as a local variable that gets deallocated before the animation runs. Store it as a property on the presenting controller.

Developers also sometimes mix modal custom transitions and navigation pushes in the same conceptual flow without a clear reason. That creates motion that feels inconsistent.

Finally, test rotation, safe area behavior, and iPad presentation. Frame-based animation that looks good in one layout can be awkward in another if you never validate the final frames.

Summary

  • Use a custom animator and transitioning delegate to present a controller from right to left.
  • Set modalPresentationStyle to .custom before presenting.
  • Keep the transitioning delegate alive as a strong property.
  • Implement both presentation and dismissal so the motion feels complete.
  • Use this pattern for modal flows that need horizontal motion, not as a replacement for normal navigation pushes.

Course illustration
Course illustration

All Rights Reserved.