如何在 ForEach 上进行单独选择

How to make an individual selection on a ForEach

我正在尝试为用户选择的每张食物图片添加复选标记,但它在每个选项上都被选中,而不是只有一个。

我知道 ForEach 可能是导致此问题的原因。但是我想不出解决这个问题的方法。

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"))
]

保留所选 comidas 的参考并使用它来确定 comidas 的已检查状态。在这种情况下,我更喜欢使用 Set,因为它会自动处理重复项并简化 adding/removing 一个项目。

第一件事是添加 Hashable 一致性到 Comida 以便我们可以用它做 Set 操作。 Comida 结构中的所有属性都已经符合 Hashable 所以声明 Hashable 一致性是你所需要的。

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

在您看来,添加一个 @State 变量来跟踪 user-selected Comidas

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

在 comida Image 上的点击手势中,在 comidasEligidas Set

中添加或删除 comida
.onTapGesture {
    if comidasEligidas.contains(comida) {
        comidasEligidas.remove(comida)
    } else {
        comidasEligidas.insert(comida)
    }
}

根据集合中是否存在 Comida 切换复选标记的不透明度,并指示观察动画的值是 eligidasComidas Set。另外,我建议在复选标记上不允许 hitTesting,这样它就不会吞下任何触摸。

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

这是完整的代码片段:

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