I am working on SwiftData project. I found the problem when adding new items to a NavigationSplitView that the first item was added without a problem. The selection was changed so that the Detail page was shown without any problem. But from then on it kept the old selection or was turned to nil, hence showing the view for a not selected item.
I tested it without SwiftData and was surprised, that it was a persistent problem. Here is the source code:
The data model is pretty simple:
struct Model: Identifiable, Hashable {
var id: UUID
var name: String
}
The content view with the navigation part is kept short:
import SwiftUI
struct ContentView: View {
@State private var data: [Model] = [
Model(id: UUID(), name: "Alfred"),
Model(id: UUID(), name: "Bart"),
Model(id: UUID(), name: "Cecille"),
Model(id: UUID(), name: "Dave")]
@State private var selectedItem: Model? {
didSet {
print("value: \(selectedItem?.name ?? "nil")
from old value \(oldValue?.name ?? "nil")")
}
}
// (1) for workaround
var body: some View {
NavigationSplitView {
// label to view the value of selectedItem
Label("\(selectedItem?.name ?? "nil")", systemImage: "hare.fill")
List(selection: $selectedItem) {
ForEach(data) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: "cat")
}
}
}
} detail: {
if let selectedItem {
DetailView(modelData: selectedItem)
} else {
Text("Select item, please!")
}
}
.toolbar {
ToolbarItem {
Button(action: {
selectedItem = nil
}) {
Label("set to nil", systemImage: "eye.trianglebadge.exclamationmark")
}
}
ToolbarItem {
Button(action: {
let newItem = Model(id: UUID(), name: "Frederic")
data.append(newItem)
selectedItem = newItem // (2) for workaround
}) {
Label("Add Item", systemImage: "plus")
}
}
}
}
// (3) for workaround
}
For better visualisation I made a short screen capture: https://youtu.be/FJpYjk336xM
I found a workaround but this seems a common problem, so what am I doing wrong?
The workaround was the following: adding a tempSelection at (1)
@State private var tempAddSelection: Model?
changing (2) to use the temporary selection
tempAddSelection = newItem
and adding an onChange statement to monitor the array
.onChange(of: data) {
if let tempAddSelection {
self.selectedItem = tempAddSelection
self.tempAddSelection = nil
}
}
But I did not find a 'correct' or best practice solution. I am not quite sure if this is a recent bug or not, or if there is a better solution. Ideas would be greatly appreciated, because I spent quite a while figuring it out as far as I came now.
It's highly recommended to use the ID
type of Identifiable
for the selection
rather than the entire model type, because ForEach
uses it internally (the inferred id
parameter). And it fixes the issue
struct Model: Identifiable, Hashable {
let id = UUID()
var name: String
}
struct TestView: View {
@State private var data: [Model] = [
Model(name: "Alfred"),
Model(name: "Bart"),
Model(name: "Cecille"),
Model(name: "Dave")]
@State private var selectedItem: Model.ID? {
didSet {
print("value: \(selectedItem) from old value \(oldValue)")
}
}
// (1) for workaround
var body: some View {
NavigationSplitView {
// label to view the value of selectedItem
if let selectedModel {
Label(selectedModel.name, systemImage: "hare.fill")
} else {
Text("Nothing selected")
}
List(selection: $selectedItem) {
ForEach(data) { item in
NavigationLink(value: item) {
Label(item.name, systemImage: "cat")
}
}
}
} detail: {
if let selectedModel {
DetailView(modelData: selectedModel)
} else {
Text("Select item, please!")
}
}
.toolbar {
ToolbarItem {
Button(action: {
selectedItem = nil
}) {
Label("set to nil", systemImage: "eye.trianglebadge.exclamationmark")
}
}
ToolbarItem {
Button(action: {
let newItem = Model(name: "Frederic")
data.append(newItem)
selectedItem = newItem.id
}) {
Label("Add Item", systemImage: "plus")
}
}
}
}
private var selectedModel : Model? {
guard let selectedItem else { return nil }
return data.first(where: {$0.id == selectedItem})
}
}