Fix: View Controller Does Not Dismiss in Swift

Fix: View Controller Does Not Dismiss in Swift

Written by

in

Quick answer: Calling dismiss(animated:completion:) only works when a view controller is presented modally. If the screen was added to a navigation stack via a push segue or show action, you must call navigationController?.popViewController(animated: true) instead.

Why this happens

In UIKit, UIViewController provides distinct navigation paradigms: modal presentation and hierarchical navigation stacks. Calling dismiss(animated:completion:) requests that the system dismiss the view controller that was presented modally by the receiver or by its presenting view controller. If your view controller was not presented modally, the method does nothing.

When you transition between screens using a “Show” segue in a storyboard or call navigationController?.pushViewController(_:animated:) in code, the new controller is placed onto a UINavigationController stack. Because the view controller was pushed rather than presented modally, self.presentingViewController is often nil or points to an ancestor hosting the entire navigation container.

Another common cause occurs when calling dismiss from an embedded child view controller or a container view without delegating the dismissal to its parent. In both scenarios, the method executes without throwing a runtime error, console print statements run as expected, but the user interface remains unchanged on screen.

How to fix it

1. Pop from the navigation controller

If your screen appeared via a push segue or a standard “Show” transition inside a navigation bar interface, you must pop it off the navigation stack instead of calling dismiss.

Replace your dismissal call with popViewController(animated:):

import UIKit

class DetailViewController: UIViewController {

    @IBAction func didTapCloseButton(_ sender: UIButton) {
        // If pushed inside a UINavigationController, pop the top controller
        navigationController?.popViewController(animated: true)
    }
}

If you want to return to the very first screen in a deep navigation stack, call popToRootViewController(animated:):

@IBAction func didTapDoneButton(_ sender: UIButton) {
    navigationController?.popToRootViewController(animated: true)
}

2. Determine presentation style dynamically

If a view controller can be displayed either modally or pushed onto a navigation stack depending on the entry point (such as opening from a search result versus a tab bar), inspect the navigation hierarchy at runtime before deciding how to close it.

A reliable check examines whether the view controller is the root of a navigation stack or if it has a presenting controller:

import UIKit

extension UIViewController {
    /// Dismisses or pops the controller depending on how it was presented.
    func closeSelf(animated: Bool = true) {
        if let navController = navigationController {
            // If this controller is not the root controller on the stack, pop it
            if navController.viewControllers.first != self {
                navController.popViewController(animated: animated)
                return
            }
        }

        // Fallback for modal presentation or root controller in a modal navigation stack
        if presentingViewController != nil {
            dismiss(animated: animated, completion: nil)
        }
    }
}

You can then call this helper directly from your action handlers:

@IBAction func cancel(_ sender: Any) {
    closeSelf(animated: true)
}

3. Handle a navigation controller presented as a modal sheet

Developers often embed a view controller inside a new UINavigationController and present that entire container modally (for example, a form sheet with “Cancel” and “Save” items in the navigation bar).

In this arrangement, calling navigationController?.popViewController(animated: true) on the root screen fails because there are no earlier controllers on that specific stack. Calling dismiss on self or on navigationController dismisses the modal presentation:

import UIKit

class FormViewController: UIViewController {

    @IBAction func cancel(_ sender: UIBarButtonItem) {
        // Dismisses the enclosing modal sheet, including its navigation bar
        dismiss(animated: true, completion: nil)
    }
}

UIKit automatically walks up the hierarchy: calling dismiss(animated:completion:) on a child view controller inside a presented navigation controller routes the dismissal to the presenting controller.

4. Use an unwind segue in Interface Builder

If you use storyboards and want to dismiss across multiple modal or pushed layers without writing custom navigation stack logic, implement an unwind segue.

First, define an unwind action on your destination (presenting) view controller:

import UIKit

class HomeViewController: UIViewController {

    @IBAction func unwindToHome(_ segue: UIStoryboardSegue) {
        // Executed after returning from the dismissed screen
    }
}

In Interface Builder:
1. Open the storyboard containing the view controller you want to dismiss.
2. Control-drag from your button (or the yellow View Controller icon) to the orange Exit icon at the top of the scene.
3. Select unwindToHome: from the popup menu.

UIKit inspects the view controller hierarchy and automatically executes either a pop or a dismiss transition to return to HomeViewController.

How to prevent it

  • Track the presentation mechanism when designing transitions: use push/pop for hierarchical linear workflows and present/dismiss for standalone tasks.
  • Avoid hardcoding dismiss in view controllers designed for reuse across both modal sheets and navigation stacks.
  • Verify navigationController is not nil during debugging; if it is nil, the view controller was not embedded in or pushed onto a navigation controller.
  • Ensure dismissal calls execute on the main thread if triggered from asynchronous callbacks or network delegates.

FAQ

Why does my view controller flash black when dismissed?

This usually occurs when you call dismiss on a view controller that is already being dismissed or when the root window lacks a background view controller. Ensure you are not triggering dismissal twice and that your modal presentation style is not .overCurrentContext without a persistent background view.

Can I call dismiss from inside a completion handler?

Yes, but you must dispatch the call to the main thread using DispatchQueue.main.async if the completion handler runs on a background queue. UIKit methods that alter view hierarchies must always execute on the main actor.

What is the difference between popViewController and dismiss?

popViewController(animated:) removes the top item from an existing UINavigationController stack, sliding the previous controller back into view. dismiss(animated:completion:) destroys a modal presentation layer that was presented on top of the current screen context.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.