iOS
view controller
modally presented
navigation stack
iOS development

How to check if a view controller is presented modally or pushed on a navigation stack?

Master System Design with Codemia

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

Introduction

In UIKit, a view controller can appear because it was pushed onto a UINavigationController stack or because it was presented modally. The distinction matters because the correct exit path is usually different: pushed controllers are popped, while modal controllers are dismissed.

There Is No Single Perfect Flag

UIKit does not expose one universal property that always says "this controller is modal" or "this controller was pushed." Instead, you infer the answer from the surrounding controller relationships.

For common UIKit setups, the usual signals are:

  • if the controller is deeper than the root of a navigation stack, it was pushed
  • if the controller or its container has a presentingViewController, it is modal

That sounds simple, but container controllers create edge cases. A controller may sit inside a navigation controller that was itself presented modally, so checking only one property is often too naive.

A Practical Helper for Common UIKit Cases

The following helper covers the patterns most apps care about.

swift
1import UIKit
2
3enum PresentationKind {
4    case pushed
5    case modal
6    case rootInNavigation
7    case unknown
8}
9
10extension UIViewController {
11    var presentationKind: PresentationKind {
12        if let nav = navigationController,
13           let index = nav.viewControllers.firstIndex(of: self),
14           index > 0 {
15            return .pushed
16        }
17
18        if presentingViewController != nil {
19            return .modal
20        }
21
22        if let nav = navigationController,
23           nav.presentingViewController?.presentedViewController == nav {
24            return .modal
25        }
26
27        if navigationController != nil {
28            return .rootInNavigation
29        }
30
31        return .unknown
32    }
33}

Usage is straightforward:

swift
1switch presentationKind {
2case .pushed:
3    navigationController?.popViewController(animated: true)
4case .modal:
5    dismiss(animated: true)
6case .rootInNavigation, .unknown:
7    print("Handle separately")
8}

This is not a law of nature for every custom container hierarchy, but it works well for standard UIKit navigation flows.

Why the Navigation Stack Check Works

If a controller lives inside a navigation controller and its position is greater than zero, that is the strongest signal that it was pushed.

swift
1if let nav = navigationController,
2   let index = nav.viewControllers.firstIndex(of: self),
3   index > 0 {
4    print("This controller was pushed")
5}

That condition intentionally excludes the root controller of the navigation stack. The root controller is inside a navigation controller, but it was not pushed from within that stack. It might be the initial screen, or the whole navigation controller might have been presented modally.

Why Modal Detection Needs Container Awareness

Checking only presentingViewController != nil is not always enough. Suppose a navigation controller is presented modally, and your actual screen is the root controller inside that navigation controller. The child screen itself may not be the directly presented object.

That is why modal detection often checks the container:

swift
1if let nav = navigationController,
2   nav.presentingViewController?.presentedViewController == nav {
3    print("The navigation controller was presented modally")
4}

This catches the common case where a navigation controller wraps the screen and the whole stack was presented as a modal flow.

Use the Result to Choose the Correct Exit Behavior

The reason developers ask this question is usually not academic. They want the correct close action.

  • pushed controllers should usually call popViewController
  • modal controllers should usually call dismiss

Choosing the wrong one leads to awkward behavior. A dismiss call may do nothing for a pushed screen, while a pop call may not make sense for a modally presented flow.

It is often cleaner to make this logic explicit in one helper rather than scattering several slightly different checks throughout button handlers.

Timing Matters

Presentation relationships are usually clearest once the controller is actually in the hierarchy. If you inspect too early, such as during initialization, container relationships may not be fully set up yet.

For most practical uses, these checks are safest in lifecycle methods such as viewDidAppear or inside user actions where the controller is already on screen. That avoids false negatives caused by checking before UIKit has finished wiring the presentation chain.

Custom Containers Change the Rules

Standard UIKit containers behave predictably enough for the helper above. Custom container controllers are different. If your app uses a coordinator, a split-view arrangement, or a custom parent-child hierarchy, the push-versus-modal distinction may not map cleanly to generic UIKit checks.

In that case, the best solution is often app-specific state rather than deeper controller introspection. If your coordinator knows how the screen was shown, let the coordinator express that directly instead of making every screen guess from container structure.

Common Pitfalls

One common mistake is checking only presentingViewController != nil and assuming that covers all modal cases. It does not reliably handle controllers embedded in a modally presented navigation controller.

Another pitfall is assuming every controller inside a navigation controller was pushed. The root controller of a navigation stack is the obvious counterexample.

A third issue is running the check too early, before the controller is fully part of the hierarchy. That can produce misleading results.

Finally, do not apply generic UIKit rules blindly to custom container architectures. In those cases, explicit app-level state is often more reliable.

Summary

  • UIKit does not provide one universal "modal or pushed" flag for every hierarchy.
  • A controller deeper than index zero in a navigation stack was usually pushed.
  • Modal detection often requires checking the controller and its container, especially when a navigation controller was presented modally.
  • The practical reason to detect this is to choose correctly between pop-style and dismiss-style behavior.
  • For custom container setups, app-specific navigation state is often more reliable than generic introspection.

Course illustration
Course illustration

All Rights Reserved.