Bug in NavigationStack's back button

Originator:xiaogdgenuine
Number:rdar://FB9997893 Date Originated:Apr 28, 2023
Status:Open Resolved:
Product:SwiftUI Product Version:SwiftUI 4
Classification:Bug Reproducible:Yes
 
It seems NavigationStack have state-inconsistent problem when dimissing a scrolling view.

Steps to reproduce:
1. Create an empty SwiftUI project and paste codes below.
2. Run the app, then navigate 3 levels deep (By tapping any items in the list).
3. Scroll the current page view
4. Tap the "< Back" button while the view is still inertial scrolling.
5. Tap the "< Back" button again.
6. The app crashed with message "Fatal error: Can't remove more items from a collection than it contains".

```swift
struct ContentView: View {
    @StateObject var holder = PathHolder()

    var body: some View {
        NavigationStack(path: holder.pathBinding) {
            PageView()
                .navigationDestination(for: Int.self) { _ in PageView() }
        }
        .environmentObject(holder)
    }
}

struct PageView: View {
    @EnvironmentObject var holder: PathHolder
    @Environment(\.dismiss) var dismiss

    var body: some View {
        VStack {
            if !holder.path.isEmpty {
                // Working version
                Button("dismiss") {
                    holder.path.removeLast()
                }
                // Crash version
//                Button("dismiss") {
//                    dismiss()
//                }
            }
            ScrollView {
                VStack {
                    // Create 100 elements for scrolling
                    ForEach(0 ..< 100) { _ in
                        NavigationLink(value: 1, label: {
                            Color.green
                                .padding(.vertical)
                                .overlay(Text("\(holder.path.count)").font(.title).foregroundColor(.red))
                                .frame(maxWidth: .infinity)
                                .frame(height: 200)
                        })
                    }
                }
            }
        }
    }
}

class PathHolder: ObservableObject {
    @Published var path: [Int] = [] {
        willSet {
            print("willSet", newValue, path)
        }
        didSet {
            print("didSet", oldValue, path)
        }
    }

    var pathBinding: Binding<[Int]> {
        Binding(get: {
            print("get \(self.path)")
            return self.path
        }, set: {
            print("set", $0)
            self.path = $0
        })
    }
}
```

In the above example, if you tap on the "dismiss" button I provided instead of the default "< Back" button, the crash won't occur.

Futher investigation shows that the "dismiss" method provided by NavigationStack is actually popping view first then modify the path binding value, so if you comment-out the "Crash version" and the app will crash again when tap on "dismiss" button.

Comments


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!