swiftuiswiftui-navigationlink

NavigationLink not working with custom detail view


The boiler plate code for a Multiplatform app on Xcode 16.1 (macOS 15.1) with SwiftUI and SwiftData, has a NavigationSplitView containing a List of NavigationLinks. When the NavigationLink contents is a standard SwiftUI widget, like Text, that widget updates when selecting the different items in the List. However, if I try to add a custom view (DetailView() in the code below) to the NavigationLink contents, that custom view is not updated when selecting different items in the list. The same behaviour is seen when running in the preview canvas, and running a build.

Here is the code. It is all the Xcode boiler plate except the addition of the DetailView implementation and the DetailView added in the NavigationLink contents.

Can anyone help me understand why the DetailView is never updated with the change in List selection?

import SwiftUI
import SwiftData

@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
    
    var stringVal: String {
        timestamp.formatted(date: .numeric, time: .complete)
    }
}

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]

    var body: some View {
        NavigationSplitView {
            List {
                ForEach(items) { item in
                    NavigationLink {
                        // DetailView does NOT update with list selection change
                            DetailView(item: item)
                                .foregroundStyle(.red)
                            Divider()
                            // Text DOES update with list selection change
                            Text(item.stringVal)
                                .foregroundStyle(.blue)
                        }
                    } label: {
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                    }
                }
            }
            .navigationSplitViewColumnWidth(min: 180, ideal: 200)
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            Text("Select an item")
        }
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem)
        }
    }
}

#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true)
}


struct DetailView: View {
    @State var item: Item
    
    var body: some View {
        Text(item.stringVal)
    }
        
}

Solution

  • DetailView.item should not be a @State. Just remove @State

    struct DetailView: View {
        let item: Item
        
        var body: some View {
            Text(item.stringVal)
        }
    }
    

    @States will only be initialised once throughout the view's lifetime, and you cannot update a @State from the outside. This is why it is strongly recommended that @State properties should be private, so that you cannot accidentally pass a value to it (which almost always does not do what you intended it to do) through the automatically-generated memberwise initialiser.

    Also, this is not a documented way of using NavigationSplitView, and I don't know if it will break in the future. NavigationSplitView should be driven by the selection of a List. The detail view should always be placed in the detail: view builder. For example:

    @State private var selection: Item?
    
    var body: some View {
        NavigationSplitView {
            List(items, selection: $selection) { item in
                NavigationLink(value: item) {
                    Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                }
            }
            .navigationSplitViewColumnWidth(min: 180, ideal: 200)
            .toolbar {
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            if let item = selection {
                DetailView(item: item)
                    .foregroundStyle(.red)
                Divider()
                Text(item.stringVal)
                    .foregroundStyle(.blue)
            } else {
                Text("Select an item")
            }
        }
    }