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
dismissin view controllers designed for reuse across both modal sheets and navigation stacks. - Verify
navigationControlleris notnilduring debugging; if it isnil, 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.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
