Search code examples
swiftuilazy-loadingswiftui-navigationlinklazyvgrid

How to trigger a NavigationLink programmatically in a LazyVGrid


I have a LazyVGrid inside a NavigationView.

NavigationView {
    ScrollView {
        LazyVGrid(columns: columns) {
            ForEach(items) { item in
                NavigationLink(tag: item, selection: $displayedItem) {
                    DetailView(item)
                } label: {
                    GridItemView(item)
                }
            }
        }
    }
}

The referenced variables are defined as follows on the view:

@State var displayedItem: Item?
let columns: [GridItem] = Array(repeating: .init(.flexible()), count: 2)

Now I want to show the detail view for a specific item. I do this by simply assigning this item to the displayedItem property:

func showDetailView(for item: Item) {
    displayedItem = item
}

This works great when the respective item is visible on the LazyVGrid at the moment when I call this function. However, when the item is not visible, I first need to scroll to the item for the NavigationLink to fire. I know why this is happening (because the items are loaded lazily, it's a lazy grid after all), but I don't know how to make the LazyVGrid load the specific item when I need it.


What I've tried:

I have also tried to programmatically scroll to the target item by wrapping the entire ScrollView inside a ScrollViewReader and appending the following modifier:

.onChange(of: displayedItem) { item in
    if let item = item {
        scrollProxy.scrollTo(item.id)
    }
}

Unfortunately, this has the same problem: Scrolling to a given item doesn't work until the item is loaded.


Question:

Is there any way to make this work, i.e. to trigger a NavigationLink for an item that is not currently visible in the LazyVGrid? (It's important for me as I need this functionality to deep-link to a specific item's DetailView.)


Solution

  • An possible approach can be like in this topic - use one link somewhere in background of ScrollView and activate it by tapGesture/button from user or assigning corresponding value programmatically.

    Tested with Xcode 13.4 / iOS 15.5

    enter image description here

    Main part:

    ScrollView {
        LazyVGrid(columns: columns) {
            ForEach(items) { item in
                RoundedRectangle(cornerRadius: 16).fill(.yellow)
                    .frame(maxWidth: .infinity).aspectRatio(1, contentMode: .fit)
                    .overlay(Text("Item \(item.value)"))
                    .onTapGesture {
                        selectedItem = item
                    }
            }
        }
    }
    .padding(.horizontal)
    .background(
        NavigationLink(destination: DetailView(item: selectedItem), isActive: isActive) {
            EmptyView()
        }
    )
    .toolbar {
        Button("Random") { selectedItem = items.randomElement() }
    }
    

    Test module on GitHub