How does UINavigationController manage its stack, and where do the bar items and back button come from?
Predict first
What does viewControllers.count show through these steps?
import UIKit
let root = UIViewController()
let nav = UINavigationController(rootViewController: root)
nav.pushViewController(UIViewController(), animated: false)
nav.pushViewController(UIViewController(), animated: false)
print(nav.viewControllers.count)
nav.popViewController(animated: false)
print(nav.viewControllers.count)
nav.popToRootViewController(animated: false)
print(nav.viewControllers.count, nav.topViewController === root)
- 3, 2, then 1 true
- 2, 1, then 0 false
- 3, 2, then 2 true
Mental model. A deck of cards where the top card is the screen. Push lays a card on the deck, pop takes the top one off, and each card carries its own label for the bar.
A navigation controller holds an ordered array, viewControllers, with topViewController on screen. pushViewController(_:animated:) adds one, popViewController removes the top, popToRootViewController and popToViewController remove several, and setViewControllers(_:animated:) replaces the whole stack at once (the clean way to deep-link or restore state). Popped controllers are released, so any state they hold is gone unless you keep it elsewhere. The interactive swipe-from-the-left-edge pop works through the controller’s interactivePopGestureRecognizer, which is disabled if you hide the back button or replace it with a custom one, unless you reset its delegate.
The bar’s content is per screen: each view controller has a navigationItem (title, bar button items, large-title mode, search controller) that the navigation bar displays while it is on top. The back button’s text comes from the previous controller’s title (or its navigationItem.backButtonTitle), not from the current one. Bar styling goes through UINavigationBarAppearance, set on the controller’s navigation item or on the bar. For a controller that should hide the tab bar when pushed, set hidesBottomBarWhenPushed on it before the push.
The stack starts with the root, grows with each push, shrinks with each pop, and popToRoot leaves only the root on top.
Common traps
- Storing important state in a pushed controller. It is released on pop. Keep state in a model that outlives the screen.
- Setting
titleon the wrong controller and expecting the back button to change. The back title comes from the controller below.
They might follow up with
- How do you restore a deep screen? Build the controllers and call
setViewControllerswith the full stack. - Why does swipe-back stop working? A custom back button or hidden bar disables the system recogniser; restore its delegate or re-enable it.