How to trigger a NavigationLink programmatically in a LazyVGrid - swiftui

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.)

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
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

Related

Strange issue of "Modifying state during view update, this will cause undefined behavior"

Goal: have a SwiftUI architecture where the "add new item" and "edit existing item" are solved by the same view (EditItemView). However, for some reason, when I do this, the runtime agent complains of "Modifying state during view update, this will cause undefined behavior".
This is the code I want to use, which ensures that the EDITING of the item and ADDING a new item are handled by the same EditItemView:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.navigationBarItems(trailing:
// THIS LINE TO ADD A NEW ITEM:
NavigationLink(destination: EditItemView(item: Data.singleton.createItem(name: "New item", value: 5.0))
{
Image(systemName: "plus")
}
)
}
}
It doesn't work, leading to the issue highlighted above. I am forced to separate the functionality for Edit and Add into two distinct Views, which then works:
var body: some View {
NavigationView
{
ScrollView
{
LazyVGrid(columns: my_columns)
{
ForEach(items, id: \.id)
{
let item = $0
// THIS LINE TO EDIT AN EXISTING ITEM
NavigationLink(destination: EditItemView(item: item))
{
ItemView(item: item)
}
}
}
}
.sheet(isPresented: $isPresented)
{
// FORCED TO USE SEPARATE VIEW
AddItemView { name, value in
_ = Data.singleton.createItem(name: name, value: value)
self.isPresented = false
}
}
.navigationBarItems(trailing: Button(action: { self.isPresented.toggle()}) { Image(systemName: "plus")})
}
}
I don't understand why the code in the first version is considered to modify the state while updating view, because to me, it's sequential: new Item is created and THEN a view is shown for that Item.
Any ideas?
The destination of NavigationLink is not rendered lazily, meaning it'll get rendered when the NavigationLink itself is rendered -- not when clicked through.
The sheet code, depending on platform and SwiftUI version, may have the same issue, but apparently does not in the version you're using. Or, the closure you provide to AddItemView isn't run immediately -- since you didn't include the code, it's not clear.
To solve the issue in the first method, you can use the following SO answer which provides a lazy NavigationLink: https://stackoverflow.com/a/61234030/560942

SwiftUI: how to have image in list view not have same action as list item?

I have a list inside a view. Inside the list, items are iterated through to populate the list. When you click on each list item, I want to navigate to another view.
This is working as expected but I want to have a Button represented by a circle in each list item that can be clicked independently without navigating to the second view. Right now, clicking the circle just takes me to 2nd view. How can I accomplish this?
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink(
destination: OtherView(name: "test"),
label: {
Text("Navigate")
})
HStack {
Image("1")
.resizable()
.frame(width: 32.0, height: 32.0)
Button(action: addItem) {
Label("Add Item", systemImage: "circle")
}
}
}
}
}
}
You can not do this with List. You can use VStack or LazyVStack inside a ScrollView as an alternative solution

SwiftUI: Alternatives to using ListView?

I want to have a scrollable list that when each row is tapped, it takes you to a different view. Inside each row, I want to have a heart button that when tapped overrides the navigation behavior and just toggles a heart fill/unfill.
As an alternative to do this, would it make sense to use a ScrollView inside a NavigationView and then have each list item be a VStack?
Pseudocode hierarchy:
NavigationView {
ScrollView {
VStack {
// Button
}
}
}
Is there a better way ( or more preferred way) to accomplish this?
Use LazyVStack
let items = ["A","B"]
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
ForEach(items, id: \.self) { item in
Text(item)
}
}
}
}

LazyVGrid onTapGesture navigate to next screen swiftUI

I am quite new to swiftUI. I have created a grid view on tapping on which I want to go to next screen. But somehow I am not able to manage to push to next screen. I am doing like this:
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: gridItems, spacing: 16) {
ForEach(viewModel.pokemon) { pokemon in
PokemonCell(pokemon: pokemon, viewModel: viewModel)
.onTapGesture {
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
Text(pokemon.name)
}
}
}
}
}
.navigationTitle("Pokedex")
}
}
Upon doing like this, I am getting a warning stating
Result of 'NavigationLink<Label, Destination>' initializer is unused
Can someone please guide me, how to do this?
.onTapGesture adds an action to perform when the view recognizes a tap gesture. In your case you don't need to use .onTapGesture. If you want to go to another view when cell is tapped you need to write NavigationLink as below.
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)) {
PokemonCell(pokemon: pokemon, viewModel: viewModel)
}
If you want to use .onTapGesture, another approach is creating #State for your tapped cell's pokemon and using NavigationLink's isActive binding. So when user tap the cell it will change the #State and toggle the isActive in .onTapGesture. You may need to add another Stack (ZStack etc.) for this.
NavigationView {
ZStack {
NavigationLink("", destination: PokemonDetailView(pokemon: pokemon), isActive: $isNavigationActive).hidden()
ScrollView {
// ...

SwiftUI Navigation Bar Title doesn't appear

I'm not sure if anything changed in Beta 3, however, when trying to add the NavigationBarTitle modifier to NavigationView, it does not show the text for the title? Any ideas?
NavigationView {
List(0 ..< 20) { item in
NavigationLink(destination: Text("1")) {
Text("Navigate 1")
}
}
}.navigationBarTitle(Text("Update")).navigationBarHidden(false)
}
The list shows but no title for the list in the NavigationView
You're setting .navigationBarTitle and .navigationBarHidden on NavigationView when they should be modifiers on List instead:
NavigationView {
List(0..<20) { item in
NavigationLink(destination: Text("1")) {
Text("Navigate 1")
}
}
.navigationBarTitle("Update")
.navigationBarHidden(false)
}
You can also just remove .navigationBarHidden(false) (unless you're setting it to true in a previous view or something).
Your code works fine and the navigationBarTitle is not outdated. It must be placed above (inside the Navigation View). Yes, it is sometimes confusing, it is necessary to remember this.
To the place where you currently have it .navigationBarTitle(Text ("Update")).navigationBarHidden(false) you need to set the modifier .navigationViewStyle(StackNavigationViewStyle ()), which means that you should always show the first screen regardless of the screen size.
var body: some View {
NavigationView {
List(0 ..< 20) { item in
NavigationLink(destination: Text("1")) {
Text("Navigate 1")
}
}
.navigationBarTitle(Text("Update"), displayMode: .automatic).navigationBarHidden(false)
}
// that means only show one view at a time no matter what device I'm working
.navigationViewStyle(StackNavigationViewStyle())
}