---- Updated to provide a reproducible example ----
Following is my View file. I'd like to have each navigation link destination linked to a view model stored in a dictionary (represented by a simple string in the example).
However, the following piece of code doesn't work and each item always displays nothing, even though I tried the solution in SwiftUI NavigationLink loads destination view immediately, without clicking
struct ContentView: View {
private var indices: [Int] = [1, 2, 3, 4]
@State var strings: [Int: String] = [:]
var body: some View {
NavigationView {
List {
ForEach(indices, id: \.self) { index in
NavigationLink {
NavigationLazyView(view(for: index))
} label: {
Text("\(index)")
}
}
}
.onAppear {
indices.forEach { index in
strings[index] = "Index: \(index)"
}
print(strings.keys)
}
}
}
@ViewBuilder
func view(for index: Int) -> some View {
if let str = strings[index] {
Text(str)
}
}
}
struct NavigationLazyView<Content: View>: View {
let build: () -> Content
init(_ build: @autoclosure @escaping () -> Content) {
self.build = build
}
var body: Content {
build()
}
}
CodePudding user response:
You're working against a couple of the principals of SwiftUI just enough that things are breaking. With a couple of adjustments, you won't even need the lazy navigation link.
First, generally in SwiftUI, it's advisable to not use indices in ForEach
-- it's fragile and can lead to crashes, and more importantly, the view doesn't know to update if an item changes, since it only compares the indexes (which, if the array stays the same size, never changes).
Generally, it's best to use Identifiable
items in a ForEach
.
This, for example, works fine:
struct Item : Identifiable {
var id = UUID()
var index: Int
var string : String?
}
struct ContentView: View {
private var indices: [Int] = [1, 2, 3, 4]
@State var items: [Item] = []
var body: some View {
NavigationView {
List(items) { item in
NavigationLink {
view(for: item)
} label: {
Text("\(item.index)")
}
}
.onAppear {
items = indices.map { Item(index: $0, string: "Index: \($0)")}
}
}
}
@ViewBuilder
func view(for item: Item) -> some View {
Text(item.string ?? "Empty")
}
}
I can't say absolutely definitively what's going on with your first example, and why the lazy navigation link doesn't fix it, but my theory is that view(for:)
and strings
are getting captured by the @autoclosure
and therefore not reflecting their updated values by the time the link is actually built. This is a side effect of the list not actually updating when the @State
variable is set, due to the aforementioned issue with List
and ForEach
using non-identifiable indices.
I'm assuming that your real situation is complex enough that there are good reasons to be doing mutations in the onAppear
and storying the indices separately from the models, but just in case, to be clear and complete, the following would be an even simpler solution to the issue, if it really were a simple situation:
struct ContentView: View {
private var items: [Item] = [.init(index: 1, string: "Index 1"),.init(index: 2, string: "Index 2"),.init(index: 3, string: "Index 3"),.init(index: 4, string: "Index 4"),]
var body: some View {
NavigationView {
List(items) { item in
NavigationLink {
view(for: item)
} label: {
Text("\(item.index)")
}
}
}
}
@ViewBuilder
func view(for item: Item) -> some View {
Text(item.string ?? "Empty")
}
}