viewDidDisappear is not called on a popped view controller when switching tabs during UITabBarController's reselect pop-to-root on iOS 27

Issue Description

Hi, I would like to share an issue with UIViewController's appearance callbacks inside UITabBarController + UINavigationController on iOS 27.

When a tab containing a UINavigationController with two view controllers is re-tapped, UIKit starts its built-in animated pop-to-root. On iOS 27, if the user switches to another tab while that pop animation is still in flight, the popped view controller receives viewWillDisappear: (with isMovingFromParent == true), but viewDidDisappear: is never called on it. The appearance callbacks stay unbalanced.

This breaks code that relies on viewDidDisappear: + isMovingFromParent to detect that a view controller was popped.

Steps to Reproduce

  1. Create a UITabBarController with two tabs. The first tab is a UINavigationController.
  2. In the first tab, push a second view controller ("Nested").
  3. Tap the first tab item again. UIKit starts the animated pop-to-root.
  4. Immediately (while the pop animation is still running), tap the second tab item.

Expected vs. Actual Behavior

  • Expected: On iOS 26, "Nested" receives viewWillDisappear: and then viewDidDisappear:, both with isMovingFromParent == true.
  • Actual: On iOS 27, "Nested" receives only viewWillDisappear:. viewDidDisappear: is NOT called, even though "Nested" is no longer in navigationController.viewControllers.

Note: On iOS 27, if the second tab is tapped after the pop animation has finished, viewDidDisappear: is delivered correctly. The issue only occurs when the tab switch happens during the pop transition.

Environment

  • Xcode Version 27.0
  • iOS 27.0 iPhone 18 Pro simulator: reproduces
  • iOS 26.0 iPhone 17 Pro simulator: does not reproduce
  • Feedback Assistant Report ID: FB24934469

Minimal Reproduction Example

// SceneDelegate.swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

  var window: UIWindow?

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = scene as? UIWindowScene else { return }
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = TabBarController()
    window.makeKeyAndVisible()
    self.window = window
  }
}

// TabBarController.swift
import UIKit

final class TabBarController: UITabBarController {

  override func viewDidLoad() {
    super.viewDidLoad()

    let firstNav = UINavigationController(rootViewController: HomeViewController())
    firstNav.tabBarItem = UITabBarItem(title: "First", image: UIImage(systemName: "house"), tag: 0)

    let second = LoggingViewController(name: "Second")
    second.tabBarItem = UITabBarItem(title: "Second", image: UIImage(systemName: "star"), tag: 1)

    viewControllers = [firstNav, second]
  }
}

class LoggingViewController: UIViewController {

  let name: String

  init(name: String) {
    self.name = name
    super.init(nibName: nil, bundle: nil)
    title = name
  }

  required init?(coder: NSCoder) { fatalError() }

  override func viewDidLoad() {
    super.viewDidLoad()
    view.backgroundColor = .systemBackground
  }

  override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    print("[\(name)] viewWillDisappear isMovingFromParent=\(isMovingFromParent)")
  }

  override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    print("[\(name)] viewDidDisappear isMovingFromParent=\(isMovingFromParent)")
  }
}

final class HomeViewController: LoggingViewController {

  init() { super.init(name: "Home") }

  required init?(coder: NSCoder) { fatalError() }

  override func viewDidLoad() {
    super.viewDidLoad()
    navigationItem.rightBarButtonItem = UIBarButtonItem(
      title: "Push",
      primaryAction: UIAction { [weak self] _ in
        self?.navigationController?.pushViewController(LoggingViewController(name: "Nested"), animated: true)
      }
    )
  }
}
viewDidDisappear is not called on a popped view controller when switching tabs during UITabBarController's reselect pop-to-root on iOS 27
 
 
Q