0

---- 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()
    }
}
Sheffield
  • 355
  • 4
  • 18

1 Answers1

1

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")
    }
}
jnpdx
  • 45,847
  • 6
  • 64
  • 94
  • Thanks a lot. I suddenly realized the problem of my original code that the ForEach actually will not get any updates because the value I pass in never changes. Actually I set `Int` as the key because it is exactly the ID of another entity I want to use as reference in the dictionary. Thanks anyway. – Sheffield Feb 20 '22 at 09:25
  • If you look at my answer, I think you’ll see that it explains that about the ForEach not changing because the input never updates. – jnpdx Feb 20 '22 at 14:42