Home > Software design >  How to make an individual selection on a ForEach
How to make an individual selection on a ForEach

Time:03-11

I'm trying to add a checkmark to every food picture the user selects, but it gets selected on every option instead of just one.

I understand it's the ForEach that may be causing this. But I can't think of a way to fix this.

The problem on the Simulator.

ScrollView(.horizontal, showsIndicators: false) {
            HStack(alignment: .bottom) {
                
                ForEach(comidas) { comida in
                    
                    VStack(alignment: .center) {
                        
                        ZStack(alignment: .center) {
                            Image(uiImage: comida.foodImage)
                                .resizable()
                                .blur(radius: 2)
                                .frame(width: 200, height: 200)
                                .cornerRadius(10)
                                .onTapGesture {
                                    withAnimation {
                                        isChecked.toggle()
                                    }
                                }
                            
                            Text(comida.name)
                                .font(.title2)
                                .fontWeight(.bold)
                                .foregroundColor(.white)
                            
                            Image(systemName: "checkmark.circle")
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .foregroundColor(Color.white)
                                .frame(width: 100, height: 100)
                                .opacity(isChecked ? 1 : 0 )
                                .animation(.easeIn(duration: 0.5))
                        }
                    }
                    .padding(.horizontal, 3)
                }
            }
        }

//"comidas" is food in Spanish

struct Comidas: Identifiable {
    var id: Int
    
    let name: String
    let foodImage: UIImage
}

let comidas = [
    Comidas(id: 0, name: "Asado", foodImage: UIImage(imageLiteralResourceName: "asado")),
    Comidas(id: 1, name: "Pizzas", foodImage: UIImage(imageLiteralResourceName: "pizzas")),
    Comidas(id: 2, name: "Milanesas", foodImage: UIImage(imageLiteralResourceName: "milanesas")),
    Comidas(id: 3, name: "Empanadas", foodImage: UIImage(imageLiteralResourceName: "empanadas")),
    Comidas(id: 4, name: "Pastas", foodImage: UIImage(imageLiteralResourceName: "pasta")),
    Comidas(id: 5, name: "Sushi", foodImage: UIImage(imageLiteralResourceName: "sushi")),
    Comidas(id: 6, name: "Facturas", foodImage: UIImage(imageLiteralResourceName: "facturas")),
    Comidas(id: 7, name: "Café", foodImage: UIImage(imageLiteralResourceName: "cafe")),
    Comidas(id: 8, name: "Helados", foodImage: UIImage(imageLiteralResourceName: "helados"))
]

CodePudding user response:

You could try this approach, using an ObservableObject for your comidas array, and a var isChecked in the Comidas struct. This works well for me.

struct ContentView: View {
    @StateObject var comidasModel = ComidasModel()  // <-- here
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(alignment: .bottom) {
                
                ForEach(comidasModel.comidas) { comida in   // <-- here
                    
                    VStack(alignment: .center) {
                        
                        ZStack(alignment: .center) {
                            Image(uiImage: comida.foodImage)
                                .resizable()
                                .blur(radius: 2)
                                .frame(width: 200, height: 200)
                                .cornerRadius(10)
                                .onTapGesture {
                                    withAnimation {
                                        comidasModel.toggleChecked(for: comida)   // <-- here
                                    }
                                }
                            
                            Text(comida.name)
                                .font(.title2)
                                .fontWeight(.bold)
                                .foregroundColor(.white)
                            
                            Image(systemName: "checkmark.circle")
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .foregroundColor(Color.white)
                                .frame(width: 100, height: 100)
                                .opacity(comida.isChecked ? 1 : 0 )
                                .animation(.easeIn(duration: 0.5))
                                // enable click on the checkmark circle
                                .onTapGesture {
                                    withAnimation {
                                        comidasModel.toggleChecked(for: comida)
                                    }
                                }
                        }
                    }
                    .padding(.horizontal, 3)
                }
            }
        }
    }
    
}

class ComidasModel: ObservableObject {
    @Published var comidas = [
        Comidas(id: 0, name: "Asado", foodImage: UIImage(imageLiteralResourceName: "asado")),
        Comidas(id: 1, name: "Pizzas", foodImage: UIImage(imageLiteralResourceName: "pizzas")),
        Comidas(id: 2, name: "Milanesas", foodImage: UIImage(imageLiteralResourceName: "milanesas")),
        Comidas(id: 3, name: "Empanadas", foodImage: UIImage(imageLiteralResourceName: "empanadas")),
        Comidas(id: 4, name: "Pastas", foodImage: UIImage(imageLiteralResourceName: "pasta")),
        Comidas(id: 5, name: "Sushi", foodImage: UIImage(imageLiteralResourceName: "sushi")),
        Comidas(id: 6, name: "Facturas", foodImage: UIImage(imageLiteralResourceName: "facturas")),
        Comidas(id: 7, name: "Café", foodImage: UIImage(imageLiteralResourceName: "cafe")),
        Comidas(id: 8, name: "Helados", foodImage: UIImage(imageLiteralResourceName: "helados"))
    ]

    func toggleChecked(for comida: Comidas) {
        if let ndx = comidas.firstIndex(of: comida) {
            comidas[ndx].isChecked.toggle()
        }
    }
}

struct Comidas: Identifiable, Equatable {
    var id: Int
    var isChecked: Bool = false  // <-- here
    let name: String
    let foodImage: UIImage
}

CodePudding user response:

Keep a reference of the selected comidas and use that to determine a comida's checked status. I prefer using a Set in this instance because it automatically handles duplicates and simplifies adding/removing an item.

First thing is to add Hashable conformance to Comida so that we can do Set operations with it. All the properties inside the Comida struct already conform to Hashable so declaring Hashable conformance is all you need.

//note that comida should be singular since it represents a single item
struct Comida: Identifiable, Hashable { 

In your view, add a @State variable to track the user-selected Comidas

@State var comidasEligidas: Set<Comida> = []

In your tap gesture on the comida Image, add or remove the comida from the comidasEligidas Set

.onTapGesture {
    if comidasEligidas.contains(comida) {
        comidasEligidas.remove(comida)
    } else {
        comidasEligidas.insert(comida)
    }
}

Toggle the opacity of the check mark depending on whether the Comida is present in the set, and indicate that the value for which to observe animation is the eligidasComidas Set. Also, I recommend disallowing hitTesting on the check mark so that it doesn't swallow any touches.

.opacity(comidasEligidas.contains(comida) ? 1 : 0 )
.animation(.easeIn(duration: 0.5), value: comidasEligidas)
.allowsHitTesting(false)

Here is the full code snippet:

struct ContentView: View {
    struct Comida: Identifiable, Hashable {
        var id: Int
        let name: String
        let foodImage: UIImage
    }

    let comidas = [
        Comida(id: 0, name: "Asado", foodImage: UIImage(imageLiteralResourceName: "asado")),
        Comida(id: 1, name: "Pizzas", foodImage: UIImage(imageLiteralResourceName: "pizzas")),
        Comida(id: 2, name: "Milanesas", foodImage: UIImage(imageLiteralResourceName: "milanesas")),
        Comida(id: 3, name: "Empanadas", foodImage: UIImage(imageLiteralResourceName: "empanadas")),
        Comida(id: 4, name: "Pastas", foodImage: UIImage(imageLiteralResourceName: "pasta")),
        Comida(id: 5, name: "Sushi", foodImage: UIImage(imageLiteralResourceName: "sushi")),
        Comida(id: 6, name: "Facturas", foodImage: UIImage(imageLiteralResourceName: "facturas")),
        Comida(id: 7, name: "Café", foodImage: UIImage(imageLiteralResourceName: "cafe")),
        Comida(id: 8, name: "Helados", foodImage: UIImage(imageLiteralResourceName: "helados"))
    ]

    @State var comidasEligidas: Set<Comida> = []

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(alignment: .bottom) {
            
                ForEach(comidas) { comida in
                
                    VStack(alignment: .center) {
                    
                        ZStack(alignment: .center) {
                            Image(uiImage: comida.foodImage)
                                .resizable()
                                .blur(radius: 2)
                                .frame(width: 200, height: 200)
                                .cornerRadius(10)
                                .onTapGesture {
                                    if comidasEligidas.contains(comida) {
                                        comidasEligidas.remove(comida)
                                    } else {
                                         comidasEligidas.insert(comida)
                                    }
                                }
                        
                            Text(comida.name)
                                .font(.title2)
                                .fontWeight(.bold)
                                .foregroundColor(.white)
                        
                            Image(systemName: "checkmark.circle")
                                .resizable()
                                .aspectRatio(contentMode: .fit)
                                .foregroundColor(Color.white)
                                .frame(width: 100, height: 100)
                                .opacity(comidasEligidas.contains(comida) ? 1 : 0 )
                                .animation(.easeIn(duration: 0.5), value: comidasEligidas)
                                .allowsHitTesting(false)
                        }
                    }
                    .padding(.horizontal, 3)
                }
            }
        }
    }
}
  • Related