Home > Blockchain >  How can I use lazy load for NavigationLink correctly?
How can I use lazy load for NavigationLink correctly?

Time:02-21

---- 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")
    }
}
  • Related