Skip to content

Navigation state is not reset when popping multiple view controllers #323

@sk409

Description

@sk409

Description

When using navigationDestination(item:content:), navigating back multiple screens with UINavigationController.popToRootViewController(animated:) does not correctly reset the navigation state.

Specifically, UIKitNavigation_viewDidDisappear is not called for intermediate screens, which causes their navigation state to remain active.

As a result, it becomes impossible to navigate to those screens again.

Proposed Solution

Instead of relying on:

  • UIKitNavigation_viewDidDisappear
  • isMovingFromParentViewController

I propose using:

  • didMove(toParent:)

This method is reliably called when a view controller is removed from its parent, including cases where multiple view controllers are popped at once.

Reproduction

  • A minimal reproduction project.

https://github.com/sk409/SwiftUINavigationSample

  • The main code of the project
import SwiftUI
import UIKitNavigation

struct ContentView: UIViewControllerRepresentable {
    @State var a = A()
    
    func makeUIViewController(context: Context) -> some UIViewController {
        UINavigationController(rootViewController: ViewControllerA(a: a))
    }
    
    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}

@Observable
class A {
    var b: B?
}

class ViewControllerA: UIViewController {
    @UIBindable var a: A
    
    init(a: A) {
        self.a = a
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton()
        button.setTitle("To B", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.frame = view.frame
        button.addAction(
            .init { [weak self] _ in
                guard let self else { return }
                a.b = .init()
            },
            for: .touchUpInside
        )
        view.addSubview(button)
        
        navigationDestination(item: $a.b) {
            ViewControllerB(b: $0)
        }
    }
}

@Observable
class B {
    var c: C?
}

class ViewControllerB: UIViewController {
    @UIBindable var b: B
    
    init(b: B) {
        self.b = b
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton()
        button.setTitle("To C", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.frame = view.frame
        button.addAction(
            .init { [weak self] _ in
                guard let self else { return }
                b.c = .init()
            },
            for: .touchUpInside
        )
        view.addSubview(button)
        
        navigationDestination(item: $b.c) {
            ViewControllerC(c: $0)
        }
    }
}

@Observable
class C {}

class ViewControllerC: UIViewController {
    let c: C
    
    init(c: C) {
        self.c = c
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let button = UIButton()
        button.setTitle("Back to A", for: .normal)
        button.setTitleColor(.black, for: .normal)
        button.frame = view.frame
        button.addAction(
            .init { [weak self] _ in
                guard let self else {
                    return
                }
                navigationController?.popToRootViewController(animated: true)
            },
            for: .touchUpInside
        )
        view.addSubview(button)
    }
}

#Preview {
    ContentView()
}

Please let me know if you need any additional details or adjustments to the reproduction setup.

Checklist

  • I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

_UIKitNavigation_onDismiss should be called for all popped view controllers, including intermediate ones, so that their navigation state is properly reset.

Actual behavior

Only the last visible screen receives _UIKitNavigation_onDismiss.

Intermediate screens do not, leaving their navigation state uncleared.

Steps to reproduce

  1. Navigate from A → B → C
  2. Call popToRootViewController(animated:) to return from C to A
  3. _UIKitNavigation_onDismiss is not called on B

Because A’s state is not reset, navigating to B again does not work as expected.

SwiftUI Navigation version information

2.6.0

Destination operating system

iOS 16, iOS 17, iOS 18

Xcode version information

Xcode 16.2

Swift Compiler version information

swift-driver version: 1.115.1 Apple Swift version 6.0.3 (swiftlang-6.0.3.1.10 clang-1600.0.30.1)
Target: arm64-apple-macosx15.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions