navigationController?.popViewController(animated: true). If the current view controller was presented modally, call dismiss(animated: true, completion: nil) instead.In UIKit, returning to an earlier screen depends entirely on how the current view controller arrived on screen. Pushing a new instance via a segue or creating a forward push transition does not reverse navigation; instead, it duplicates controllers and consumes memory.
Here is how to properly navigate back in Swift whether your view controller lives in a UINavigationController stack or was presented as a modal sheet.
Why this happens
When developers want to return to a previous screen, a common mistake is triggering a new push or segue forward (for example, performSegue(withIdentifier: "back", sender: nil)). In UIKit, navigation transitions do not automatically detect that a target view controller already exists in memory.
Executing a forward push creates a brand-new instance of the destination controller and places it on top of the active hierarchy. Over time, this leads to an endlessly growing navigation stack, severe memory leaks, and broken back buttons.
UIKit separates presentation models into two primary categories:
1. Hierarchical navigation stacks: Controlled by UINavigationController, where controllers are pushed and popped.
2. Modal presentations: Controlled directly by a presenting UIViewController, where views slide up or appear as sheets and must be dismissed.
Calling the wrong method (such as attempting popViewController on a modally presented controller that lacks a navigation container) does nothing because navigationController evaluates to nil.
How to fix it
1. Pop the top view controller from a navigation stack
If your screen was pushed using UINavigationController.pushViewController(_:animated:) or a standard Show/Push segue, call popViewController(animated:).
import UIKit
class DetailViewController: UIViewController {
@IBAction func backButtonTapped(_ sender: UIButton) {
navigationController?.popViewController(animated: true)
}
}
If you are handling a selection inside a UITableViewController, place the pop call inside tableView(_:didSelectRowAt:):
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
// Return to the previous screen immediately after row tap
navigationController?.popViewController(animated: true)
}
popViewController(animated:) is marked with @discardableResult in modern Swift SDKs, so you do not need to assign its return value to _.
2. Dismiss a modally presented view controller
If the view controller was presented using present(_:animated:completion:) or a Present Modally storyboard segue, it does not live on an enclosing navigation stack. Call dismiss(animated:completion:) directly on the presented controller:
import UIKit
class ModalSettingsViewController: UIViewController {
@IBAction func closeButtonTapped(_ sender: UIButton) {
dismiss(animated: true, completion: nil)
}
}
UIKit automatically routes this call to the presenting view controller, which removes the modal presentation and frees its memory.
3. Handle hybrid setups (embedded navigation controllers)
Sometimes a view controller is embedded in its own navigation controller and presented modally. In this setup:
– Calling popViewController(animated:) only works if there is more than one controller already in that modal stack.
– If it is the root of the modal presentation, popping has no effect; you must call dismiss(animated:completion:).
You can handle both states dynamically:
func closeOrGoBack() {
if let navController = navigationController, navController.viewControllers.count > 1 {
navController.popViewController(animated: true)
} else {
dismiss(animated: true, completion: nil)
}
}
4. Jump directly to the root view controller
To skip multiple layers of pushed screens and jump directly back to the beginning of the navigation stack, use popToRootViewController(animated:):
@IBAction func returnHomeTapped(_ sender: UIButton) {
navigationController?.popToRootViewController(animated: true)
}
5. Return to a specific view controller in the stack
If you need to return to an intermediate view controller rather than the root or immediate predecessor, inspect the viewControllers array on UINavigationController and pass the target instance to popToViewController(_:animated:):
func returnToProfile() {
guard let navController = navigationController else { return }
for controller in navController.viewControllers {
if let profileVC = controller as? ProfileViewController {
navController.popToViewController(profileVC, animated: true)
break
}
}
}
6. Use an unwind segue (Storyboard projects)
If your application relies on Interface Builder and Storyboards, the cleanest declarative way to jump back across one or more screens is an unwind segue.
- Open the destination view controller (the screen you want to return to).
- Add an unwind action method:
@IBAction func unwindToMain(segue: UIStoryboardSegue) {
// Optional: read data sent from the source controller
}
- In your storyboard on the originating screen, Control-drag from a button (or the screen’s View Controller icon) up to the screen’s Exit icon.
- Select
unwindToMainWithSegue:from the popup menu. - If triggered programmatically, select the unwind segue in Document Outline, assign an Identifier (such as
"unwindToMain"), and call:
performSegue(withIdentifier: "unwindToMain", sender: self)
How to prevent it
- Never create forward segues or push transitions with labels like “Back” to navigate upward in the UI hierarchy.
- Verify whether a view controller is pushed or presented before deciding between
popViewController(animated:)anddismiss(animated:completion:). - Pass data back through delegates, closures, or notification observers rather than instantiating the previous controller again.
- Avoid strong reference cycles in completion handlers when calling
dismissby using[weak self]where appropriate.
FAQ
Why does navigationController?.popViewController do nothing?
This occurs when the view controller was displayed using a modal presentation rather than being pushed onto a UINavigationController. In modal contexts without an enclosing navigation controller, self.navigationController is nil, so the call is ignored.
How can I pass data back when returning to the previous screen?
Define a delegate protocol on the child controller or use a completion closure callback. Assign the parent controller as the delegate before pushing or presenting, then call the delegate method immediately before popping or dismissing.
What is the difference between popViewController and dismiss?
popViewController(animated:) operates on a UINavigationController stack by deallocating the topmost controller and exposing the one underneath it. dismiss(animated:completion:) operates on modal presentations by tearing down the view controller that was overlaid onto the presenting controller.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
