Home > Blockchain >  DetailView only calls on first item in array when using LazyVGrid
DetailView only calls on first item in array when using LazyVGrid

Time:09-29

I had a list that I could add items to and when I click on them it opened up the correct detail view. I recently swapped the list out for LazyVGrid but when I click on a grid item, the detail view only calls on the first item in the array.

This is the list view:

import SwiftUI

struct ListView: View {

    @EnvironmentObject var listViewModel: ListViewModel
    @State var addItemView = false
    @State var detailView = false
    
    let columns = [GridItem(.flexible()), GridItem(.flexible())]

    var body: some View {
        NavigationView {
            ZStack {
                ZStack {
                    Color(.white)
                }
                VStack {
                    Button(action: {
                        self.addItemView = true
                    }, label: {
                        Text("Add")
                    }).sheet(isPresented: $addItemView, content: {
                        AddItemView()
                    })
                    LazyVGrid(columns: columns, spacing: 20) {  // <-- the only line i comment when using list
//                        List {
                        ForEach(listViewModel.item, id:\.id){ item in
                            Button(action: {
                                self.detailView = true
                            }, label: {
                                ListRowView(item: item)
                            }).sheet(isPresented: $detailView, content: {
                                DetailView(item: item)
                            })
                        }
//                            .onDelete(perform: listViewModel.deleteItem)
//                            .onMove(perform: listViewModel.moveItem)
//                            .listRowBackground(Color.blue)
                    }
//                        .listStyle(PlainListStyle())
                    Spacer()
                }
            }
        }
    }
}

struct ListRowView: View {   // <-- using this as grid item
    @State var item:Item

    var body: some View{
        VStack {
            Text(item.name).foregroundColor(.white)
        }.frame(width: 150, height: 150, alignment: .center)
        .background(Color.blue)
        .cornerRadius(10)
    }
}

The add item and detail view:

struct AddItemView: View {
    @Environment(\.presentationMode) var presentationMode
    @EnvironmentObject var listViewModel: ListViewModel
    @State var id = UUID()
    @State var name = ""

    var body: some View {
        TextField("Enter Name", text: $name)
        Button(action: {
            addItem()
        }, label: {
            Text("Done")
        })
    }

    func addItem() {
        listViewModel.addItem(id: id.uuidString, name: name)
        presentationMode.wrappedValue.dismiss()
    }
}

struct DetailView: View {
    @State var item:Item

    var body: some View {
        Text(item.name)
    }
}

And this is how im adding each item:

import Foundation

struct Item: Hashable, Codable, Equatable {
    var id:String
    var name: String
}

class ListViewModel: ObservableObject {

    @Published var item: [Item] = [] {
        didSet {
            saveItem()
        }
    }

    let itemsKey: String = "items_key"

    init() {
        getItems()
    }

    func getItems() {
        guard
            let data = UserDefaults.standard.data(forKey: itemsKey),
            let savedItems = try? JSONDecoder().decode([Item].self, from: data)
        else { return }
    
        self.item = savedItems
    }

    func deleteItem(indexSet: IndexSet){
        item.remove(atOffsets: indexSet)
    }

    func moveItem(from: IndexSet, to: Int){
        item.move(fromOffsets: from, toOffset: to)
    }

    func addItem(id: String, name: String){
        let newItem = Item(id: id, name: name)
        item.append(newItem)
        print(newItem)
    }

    func saveItem() {
        if let encodedData = try? JSONEncoder().encode(item) {
            UserDefaults.standard.set(encodedData, forKey: itemsKey)
        }
    }
}

I'm not sure why the LazyVGrid is only calling on the first item, any help would be appreciated

CodePudding user response:

The problem is here:

@State var detailView = false /// will only work for 1 sheet, not multiple...

...

ForEach(listViewModel.item, id:\.id){ item in
    Button(action: {
        self.detailView = true
    }, label: {
        ListRowView(item: item)
    }).sheet(isPresented: $detailView, content: { /// not good to have `sheet` inside ForEach
        DetailView(item: item)
    })
}

In each iteration of the ForEach, you have a sheet. That's a lot of sheets... and when detailView is set to true, all of them will try to present. By chance, the first once gets presented.

Instead, you'll need to use sheet(item:onDismiss:content:). This alternate version of sheet is specifically made for your purpose — when you have an array of Items and want to present a sheet for that specific Item.

First, you'll need to make Item conform to Identifiable.

                                           /// here!
struct Item: Hashable, Codable, Equatable, Identifiable {

Then, replace the old trigger, @State var detailView = false, with @State var selectedDetailItem: Item?. Also, make sure to move the sheet outside of the ForEach, so it doesn't repeat. Now, the sheet will only present when selectedDetailItem is not nil.

struct ListView: View {

    @EnvironmentObject var listViewModel: ListViewModel
    @State var addItemView = false
    @State var selectedDetailItem: Item? /// here!
    
    let columns = [GridItem(.flexible()), GridItem(.flexible())]

    var body: some View {
        NavigationView {
            ZStack {
                ZStack {
                    Color(.white)
                }
                VStack {
                    Button(action: {
                        self.addItemView = true
                    }, label: {
                        Text("Add")
                    }).sheet(isPresented: $addItemView, content: {
                        AddItemView()
                    })
                    LazyVGrid(columns: columns, spacing: 20) {
                        ForEach(listViewModel.item, id:\.id){ item in
                            Button(action: {
                                self.selectedDetailItem = item /// set the selected item
                            }, label: {
                                ListRowView(item: item)
                            })
                        }
                    }
                    Spacer()
                }
            }
        } /// present the sheet here
        .sheet(item: $selectedDetailItem) { selectedItem in
            DetailView(item: selectedItem)
        }
    }
}

Result:

Detail view gets presented with the right text
  • Related