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)
}
}
DetailView.item
should not be a @State
. Just remove @State
struct DetailView: View {
let item: Item
var body: some View {
Text(item.stringVal)
}
}
@State
s 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")
}
}
}