iOS
Swift
ViewController
ModalPresentation
NavigationController

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, the same view controller class can appear in different ways: pushed onto a navigation stack, presented modally, embedded inside a navigation controller that was presented modally, or hosted inside a tab bar controller. That is why checking how a screen arrived is slightly more subtle than one single property lookup.

The practical answer is to inspect the surrounding controller relationships and choose the rule that matches your UI architecture. There is no single universal boolean built into UIKit for "modal versus pushed."

The Simplest Modal Check

If a view controller was directly presented, presentingViewController is often enough:

swift
let isPresentedModally = presentingViewController != nil

That works for many simple cases, but it is incomplete. A pushed view controller inside a navigation controller that itself was presented modally can still have different relationship values than a directly presented controller.

A More Robust Helper

A common UIKit helper looks like this:

swift
1import UIKit
2
3extension UIViewController {
4    var isModal: Bool {
5        if presentingViewController != nil {
6            return true
7        }
8
9        if let navigationController = navigationController,
10           navigationController.presentingViewController?.presentedViewController == navigationController {
11            return true
12        }
13
14        if let tabBarController = tabBarController,
15           tabBarController.presentingViewController is UITabBarController {
16            return true
17        }
18
19        return false
20    }
21}

Usage:

swift
1if isModal {
2    dismiss(animated: true)
3} else {
4    navigationController?.popViewController(animated: true)
5}

This handles the most common modal-container cases better than a single property check.

Detect Whether It Was Pushed

If the controller is inside a navigation controller and is not the root, it was pushed:

swift
let wasPushed = navigationController?.viewControllers.first != self

Or more explicitly:

swift
let wasPushed = navigationController?.viewControllers.contains(self) == true
    && navigationController?.presentingViewController == nil

However, the exact meaning of "pushed" becomes tricky if the whole navigation controller itself was presented modally. In that case, the screen is pushed inside a modal flow.

Why Context Matters

Suppose this happens:

  1. present a UINavigationController modally
  2. push several screens inside it

Those inner screens are both:

  • part of a navigation stack
  • inside a modal presentation flow

So you need to decide which question you are asking:

  • was this exact controller pushed inside its navigation controller?
  • is this screen currently participating in a modal flow overall?

UIKit can support both answers, but your back-button logic must choose one definition.

Use the Check for Dismissal Logic Carefully

A common use case is deciding whether a close button should dismiss or pop:

swift
1@IBAction func closeTapped(_ sender: UIButton) {
2    if isModal {
3        dismiss(animated: true)
4    } else {
5        navigationController?.popViewController(animated: true)
6    }
7}

This is fine as long as your app's navigation patterns are consistent. If you mix modal flows, tabs, split views, and deep navigation stacks, centralizing the logic in one helper is usually safer than repeating ad hoc checks throughout the app.

Common Pitfalls

  • Assuming presentingViewController != nil covers every modal-related case.
  • Forgetting that a navigation controller can be presented modally while its child screens are pushed.
  • Using one simplistic rule in an app with mixed tabs, modals, and embedded navigation controllers.
  • Treating "pushed" and "part of a modal flow" as if they were the same question.
  • Scattering presentation checks around the codebase instead of centralizing them in one helper.

Summary

  • UIKit does not provide one perfect built-in flag for modal versus pushed.
  • 'presentingViewController is a useful start but not the whole story.'
  • A helper that checks navigation and tab bar container relationships is more reliable.
  • Decide whether you care about direct presentation style or overall flow context.
  • Centralize the rule if the app uses several different navigation patterns.

Course illustration
Course illustration

All Rights Reserved.