How does UINavigationController manage its stack, and where do the bar items and back button come from?

7.4.1 · L2 · Interview

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 title on 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 setViewControllers with 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.

Related questions

  • 7.3.1 What is the view controller lifecycle, and what belongs in each method?
  • 7.4.2 How do UITabBarController, UISplitViewController and UIPageViewController differ, and what loads when?
  • 7.7.3 How do dynamic colours, dark mode and the UIAppearance proxy work?

How do UITabBarController, UISplitViewController and… →