How to I pass a specific value within a ForEach loop

52 Views Asked by At

Noob here, I'm displaying a list in a table of sorts. When I select one of the items in the list, I want to pass an identifier for that selected item to a sheet so I can display some data related to the item selected. The problem I'm having is that each iteration of the loop changes the value of the identifier I need to pass. So when I click on a specific item in the list, the value being passed is that of the last identifier in the array not the identifier for the item I have selected.

Here is some code that replicates the issue I'm having.

struct Test: View {
    @StateObject var vm = ViewModel()
    
    var x = 0
    
    var body: some View {
        VStack {
            ForEach (vm.someData, id: \.self) { row in
                Button {
                    vm.showSheet.toggle()
                } label: {
                    Text("(\(row))")
                }
                .onChange(of: vm.showSheet) { _ in
                    if vm.showSheet {
                        vm.theIdentifer = row
                    }
                }
                .sheet(isPresented: $vm.showSheet) {
                    TheSheet(vm: vm)
                }
            }
        }
        .padding()
    }
}

extension Test {
    @MainActor class ViewModel: ObservableObject {
        @Published var showSheet = false
        @Published var theIdentifer = 0
        
        @Published var someData = [ 2, 4, 6, 8, 10, 12 ]
    }
}

struct TheSheet: View {
    @ObservedObject var vm: Test.ViewModel
    
    var body: some View {
        VStack {
            Text("(\(vm.theIdentifer))")
            Button {
                vm.showSheet.toggle()
            } label: {
                Text("Close")
            }
        }
        .frame(width: 400, height: 200)
    }
}

What's the solution here? Thanks in advance

1

There are 1 best solutions below

1
tanmoy On

Working solution:

struct Test: View {
    @StateObject var vm = ViewModel()
    
    var body: some View {
        VStack {
            ForEach (vm.data, id: \.self) { row in
                Button {
                    vm.handleTap(for: row)
                } label: {
                    Text("(\(row))")
                }
            }.sheet(isPresented: $vm.showSheet) {
                TheSheet(id: $vm.selectedRow,
                         showSheet: $vm.showSheet)
            }
        }.padding()
    }
}

extension Test {
    @MainActor class ViewModel: ObservableObject {
        @Published var data: [Int] = [ 2, 4, 6, 8, 10, 12 ]
        @Published var selectedRow: Int = 0
        @Published var showSheet: Bool = false
        
        func handleTap(for row: Int) {
            self.showSheet = true
            self.selectedRow = row
        }
    }
}

struct TheSheet: View {
    @Binding var id: Int
    @Binding var showSheet: Bool
    
    var body: some View {
        VStack {
            Text("(\(id))")
            Button {
                showSheet = false
            } label: {
                Text("Close")
            }
        }
        .frame(width: 400, height: 200)
    }
}

Observations:

  • ViewModel should only hold data and view will dispatch all data change-related actions to ViewModel.
  • Use .sheet(isPresented: $vm.showSheet) { TheSheet(vm: VM) } outside of your ForEach loop.
  • Previously, if you set showSheet to true in your code, the top row (which is 2) content was displayed first because there was no condition to prevent this.