Search code examples
iosswiftuiswiftui-navigationsplitview

SwiftUI 4: navigationDestination()'s destination view isn't updated when state changes


While experiments with the new NavigationStack in SwiftUI 4, I find that when state changes, the destination view returned by navigationDestination() doesn't get updated. See code below.

struct ContentView: View {
    @State var data: [Int: String] = [
        1: "One",
        2: "Two",
        3: "Three",
        4: "Four"
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach(Array(data.keys).sorted(), id: \.self) { key in
                    NavigationLink("\(key)", value: key)
                }
            }
            .navigationDestination(for: Int.self) { key in
                if let value = data[key] {
                    VStack {
                        Text("This is \(value)").padding()
                        Button("Modify It") {
                            data[key] = "X"
                        }
                    }
                }
            }
        }
    }
}

Steps to reproduce the issue:

  1. Run the code and click on the first item in the list. That would bring you to the detail view of that item.

  2. The detail view shows the value of the item. It also has a button to modify the value. Click on that button. You'll observe that the value in the detail view doesn't change.

I debugged the issue by setting breakpoints at different place. My observations:

  • When I clicked the button, the code in List get executed. That's as expected.

  • But the closure passed to navigationDestination() doesn't get executed, which explains why the detail view doesn't get updated.

Does anyone know if this is a bug or expected behavior? If it's not a bug, how can I program to get the value in detail view updated?

BTW, if I go back to root view and click on the first item to go to its detail view again, the closure passed to navigationDestination() get executed and the detail view shows the modified value correctly.


Solution

  • The button is correctly changing the value. By default navigationDestination does't create a Binding relation between the parent & child making the passed values immutable.

    So you should create a separate struct for the child in order to achieve Bindable behavior:

    struct ContentView: View {
        @State var data: [Int: String] = [
            1: "One",
            2: "Two",
            3: "Three",
            4: "Four"
        ]
    
        var body: some View {
            NavigationStack {
                List {
                    ForEach(Array(data.keys).sorted(), id: \.self) { key in
                        NavigationLink("\(key)", value: key)
                    }
                }
                .navigationDestination(for: Int.self) { key in
                    SubContentView(key: key, data: $data)
                }
            }
        }
    }
    
    struct SubContentView: View {
        let key: Int
        @Binding var data: [Int: String]
        var body: some View {
            if let value = data[key] {
                VStack {
                    Text("This is \(value)").padding()
                    Button("Modify It") {
                        data[key] = "X"
                    }
                }
            }
        }
    }