NavigationStack path out of sync with UI

I am struggling to figure out how to make the NavigationStack work correctly with a completely custom navigation system.

I have a stack that is driven by a path. This path is updated by several different UI elements including a custom toolbar at the top of the screen with a back button.

The issue itself is quite simple, but I have not found a great solution. If you navigate forward and press the back button very shortly after, the path will show an array that is empty, but the screen will not be on the root view.

This particular problem has caused inconsistency and crashing in the app in the form of this exception:

NSInvalidArgumentException - <SwiftUI.UIKitNavigationController: <address>> is pushing the same view controller instance (<_TtGC7SwiftUI32NavigationStackHostingControllerVS_7AnyView_: <address>>) more than once which is not supported and is most likely an error in the application

This only happens when the navigation is animated. I can avoid the problem altogether by using:

var transaction = Transaction()
 transaction.disablesAnimations = true

 withTransaction(transaction) {
   // navigate
 }

However, this is not ideal because I want to use the animations when moving between views.

I have observed the system behavior and it appears that the back button is simply disabled while the view is transitioning. My fix so far is to add a mandatory delay between edits of the path that should pause navigation long enough for the animations to complete, but this just feels wrong and brittle.

I have put together a sample SwiftUI view that shows the issue I am having. As of now, this doesn't seem to crash in the example, but the views do get out of sync if you "Go to next" and hit "Back" in rapid succession.

struct ContentView: View {
    @State private(set) var path = [String]()
    
    var body: some View {
        VStack {
            
            NavigationStack(path: $path) {
                Spacer()
                VStack {
                    Spacer()
                    
                    Button("Go to next") {
                        path.append(UUID().uuidString)
                        print(path)
                    }
                    .padding()
                    
                    Spacer()
                }
                .navigationDestination(for: String.self) { textValue in
                    VStack {
                        Text(textValue)
                        
                        Button("Go to next") {
                            path.append(UUID().uuidString)
                        }
                    }
                }
            }
            
            Button("Back") {
                if path.count > 0 {
                    path.removeLast()
                }
                print(path)
            }
            .padding()
        }
    }
}

I appreciate any help I can get on this! I would love to keep using SwiftUI for navigation. Apart from this issue, it has worked really well.

I wonder if it's anything to do with the description in the developer documentation:

Calling this method may invalidate all saved indices of this collection. Do not rely on a previously stored index value after altering a collection with any operation that can change its length.

Would path.remove(at: path.count - 1) be better?

I had a bad idea that actually seems to work. The idea is to force the view to refresh once the path is changed. I put an id on the stack and change this whenever the path is updated. It forces the stack to go backwards regardless of what the animation state is.

I would love to get some insight into what exactly is going on with all this. It is strange to me that the path, although a state variable itself, would not force the view to refresh.

I am not confident in this solution, and it doesn't feel right either. I haven't been able to reproduce the crashes I was seeing before. Not sure if that is due to an iOS update or something.

struct ContentView: View {
    @State private(set) var path = [String]()
    @State private var id = UUID().uuidString
    
    var body: some View {
        VStack {
            
            NavigationStack(path: $path) {
                Spacer()
                VStack {
                    Spacer()
                    
                    Button("Go to next") {
                        path.append(UUID().uuidString)
                        print(path)
                    }
                    .padding()
                    
                    Spacer()
                }
                .navigationDestination(for: String.self) { textValue in
                    VStack {
                        Text(textValue)
                        
                        Button("Go to next") {
                            path.append(UUID().uuidString)
                        }
                    }
                }
            }
            .id(id)

            Button("Back") {
                if path.count > 0 {
                    path = path.dropLast()
                    id = UUID().uuidString
                }
                print(path)
            }
            .padding()
        }
    }
}

@bperrysw Please file a bug report using Feedback assistant, see Bug Reporting: How and Why?.

You workaround should be fine. You could also use the withAnimation to animate the data change.

  Button("Go to next") {
                withAnimation(.easeIn) {
                    path.append(UUID().uuidString)

                }
            }

Bug Report: FB14499431

NavigationStack path out of sync with UI
 
 
Q