iOS Development
Swift
View Controller Navigation
UIKit
Xcode

Programmatically navigate to another view controller/scene

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Programmatic navigation in UIKit is mostly about choosing the right transition type for the flow you are building. The API call is easy once that decision is clear: push when you are drilling deeper in a navigation stack, present when the new screen is a separate modal task, and replace the root controller only for major app-state changes.

Push When the Screen Belongs in the Navigation Stack

If the next screen is part of a hierarchical flow, use pushViewController on a UINavigationController.

swift
1import UIKit
2
3final class HomeViewController: UIViewController {
4    @IBAction func didTapProfile(_ sender: UIButton) {
5        let profile = ProfileViewController()
6        navigationController?.pushViewController(profile, animated: true)
7    }
8}

This preserves the normal back button and stack history. It also means the current controller must actually be embedded inside a navigation controller. If navigationController is nil, the push will do nothing.

That is one of the most common sources of confusion when "programmatic navigation" appears broken.

Present Modally for Separate Tasks

If the new screen is a self-contained task such as settings, login, or a short form, modal presentation is often the better choice.

swift
1import UIKit
2
3final class HomeViewController: UIViewController {
4    @IBAction func didTapSettings(_ sender: UIButton) {
5        let settings = SettingsViewController()
6        let nav = UINavigationController(rootViewController: settings)
7        nav.modalPresentationStyle = .formSheet
8        present(nav, animated: true)
9    }
10}

Wrapping the modal scene in its own navigation controller is a common pattern when that modal flow may push its own internal screens.

Instantiate Storyboard Scenes Explicitly

If you are using storyboards and want to navigate without segues, instantiate the destination view controller by identifier.

swift
1import UIKit
2
3final class CatalogViewController: UIViewController {
4    func openCart() {
5        let storyboard = UIStoryboard(name: "Main", bundle: nil)
6        guard let vc = storyboard.instantiateViewController(withIdentifier: "CartViewController") as? CartViewController else {
7            return
8        }
9        navigationController?.pushViewController(vc, animated: true)
10    }
11}

This is often easier to manage than mixing storyboards, segues, and code-driven transitions in inconsistent ways.

Replace the Root for App-State Changes

Sometimes the transition is not part of normal navigation at all. For example, moving from the login flow to the main app shell is usually cleaner as a root-controller replacement.

swift
1import UIKit
2
3final class SceneRouter {
4    static func setRoot(_ root: UIViewController, in window: UIWindow, animated: Bool = true) {
5        guard animated else {
6            window.rootViewController = root
7            window.makeKeyAndVisible()
8            return
9        }
10
11        UIView.transition(with: window, duration: 0.25, options: .transitionCrossDissolve) {
12            window.rootViewController = root
13            window.makeKeyAndVisible()
14        }
15    }
16}

This avoids piling modals or extra pushes onto stacks that no longer represent the real app state.

Pass Data Before Navigating

When a destination needs context, pass it explicitly before the transition.

swift
let details = ProductDetailsViewController()
details.productId = selectedId
navigationController?.pushViewController(details, animated: true)

For simple flows, setting a property is fine. For larger apps, route models, dependency injection, or coordinators often scale better.

Prevent Duplicate Transitions

Programmatic navigation can break if the user taps a button repeatedly and triggers the same transition several times before the animation completes.

swift
1@IBAction func didTapCheckout(_ sender: UIButton) {
2    sender.isEnabled = false
3    defer { sender.isEnabled = true }
4    navigationController?.pushViewController(CheckoutViewController(), animated: true)
5}

This is a practical fix for a very common class of intermittent navigation bugs.

Use Coordinators When Flows Get Complex

Once the app has many scenes and cross-cutting routes, moving navigation logic into a coordinator can reduce coupling.

swift
1protocol AppCoordinating {
2    func start()
3    func showProfile(userId: String)
4}
5
6final class AppCoordinator: AppCoordinating {
7    private let nav: UINavigationController
8
9    init(nav: UINavigationController) {
10        self.nav = nav
11    }
12
13    func start() {
14        nav.setViewControllers([HomeViewController()], animated: false)
15    }
16
17    func showProfile(userId: String) {
18        let vc = ProfileViewController()
19        vc.userId = userId
20        nav.pushViewController(vc, animated: true)
21    }
22}

You do not need this pattern for tiny apps, but it helps once routing starts spreading across many controllers.

Common Pitfalls

The most common pitfall is trying to push from a controller that is not inside a navigation controller. Another is using modal presentation when the user really expects stack-based back navigation.

Teams also often mix root replacement, pushes, and presents without deciding which one owns each flow. That leads to confusing back behavior.

Finally, if you programmatically instantiate storyboard scenes, keep identifiers centralized. Random string identifiers scattered across files are easy to break.

Summary

  • Use pushViewController for hierarchical stack navigation.
  • Use present for separate modal tasks.
  • Replace the root controller for major app-state transitions such as login to main app.
  • Pass route data explicitly before navigating.
  • As routing grows, centralize it instead of leaving every controller to navigate itself differently.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.