SwiftUI中为ObservableObject下的结构体数组创建自定义Binding
场景说明
现有一个作为EnvironmentObject的ObservableObject类Order,内部包含OrderItem(商品+数量)数组,通过列表展示这些项并使用Stepper修改每项数量。原基础Binding可正常工作,但需要实现数量不能减至0以下的限制,自定义Binding时遇到问题。
原工作代码
struct MenuItem: Codable, Hashable, Identifiable { var id: UUID var name: String var price: Int } struct OrderItem: Hashable, Identifiable { var item: MenuItem var quantity: Int var id: UUID } class Order: ObservableObject { @Published var items = [OrderItem]() } struct OrderView: View { @EnvironmentObject var order: Order var body: some View { List { Section("My Order") { ForEach($order.items) { $orderItem in VStack { Text(orderItem.item.name) HStack { Text("Quantity: \(orderItem.quantity)") Spacer() Stepper("", value: $orderItem.quantity) } } } } } } }
遇到的问题
尝试自定义Binding限制数量时,写出如下代码:
Stepper("", value: bindingForItemQuantity($orderItem)) private func bindingForItemQuantity(_ bindedOrderItem: Binding<OrderItem>) -> Binding<Int> { let oi = bindedOrderItem.wrappedValue return Binding<Int>( get: { oi.quantity }, set: { newValue in if newValue >= 1 { oi.quantity = newValue } } ) }
触发错误:Cannot assign to property: 'oi' is a 'let' constant
曾尝试将OrderItem改为类,但导致视图无法自动更新,临时通过切换State布尔值解决,希望得到更优雅的方案。
优雅解决方案
方案1:封装自定义Binding函数
无需修改OrderItem的结构体类型,直接通过Binding的wrappedValue操作原绑定对象,避免操作不可变副本:
struct OrderView: View { @EnvironmentObject var order: Order var body: some View { List { Section("My Order") { ForEach($order.items) { $orderItem in VStack { Text(orderItem.item.name) HStack { Text("Quantity: \(orderItem.quantity)") Spacer() Stepper("", value: bindingForItemQuantity($orderItem)) } } } } } } private func bindingForItemQuantity(_ orderItemBinding: Binding<OrderItem>) -> Binding<Int> { Binding<Int>( get: { orderItemBinding.wrappedValue.quantity }, set: { newValue in guard newValue >= 1 else { return } orderItemBinding.wrappedValue.quantity = newValue } ) } }
方案2:内联自定义Binding(更紧凑)
如果不想单独写函数,可直接在Stepper处内联实现:
Stepper("", value: .init( get: { orderItem.quantity }, set: { newValue in if newValue >= 1 { orderItem.quantity = newValue } } ))
方案3:结合禁用状态优化体验
为了让用户更直观感知限制,可给Stepper添加disabled条件,当数量为1时禁用减号操作(需配合Binding限制,避免其他途径修改数量):
Stepper("", value: .init( get: { orderItem.quantity }, set: { newValue in if newValue >= 1 { orderItem.quantity = newValue } } )) .disabled(orderItem.quantity <= 1)
错误原因说明
之前的代码出错是因为let oi = bindedOrderItem.wrappedValue获取的是结构体的不可变副本,修改副本不会影响原数据,且oi是let常量无法修改。正确方式是通过binding.wrappedValue直接操作原绑定对象,既能修改数据,又能触发SwiftUI的视图更新(因Order是ObservableObject,items被@Published修饰,修改数组内的结构体元素会触发变更通知)。
内容的提问来源于stack exchange,提问作者batsansierra

