SwiftUI修改@Published数组元素属性时如何强制触发视图更新
免责声明:本人为Android开发者
我目前正尝试通过KMM将现有Android应用适配到iOS平台,想要了解当仅修改数组内Item元素的属性时,是否有方法强制SwiftUI触发视图更新(效果类似Jetpack Compose中的 array[index].copy() 函数)。
当前代码实现
class MyViewModel: ObservableObject { // ... @Published var itemsArray = [Item]() // ... func increaseCount(itemIndex: Int) { let count = itemsArray[itemIndex].quantity + 1 itemsArray[itemIndex].quantity = count print("item count is \(count)") } func decreaseCount(itemIndex: Int) { var count: Int if itemsArray[itemIndex].quantity < 2 { count = 1 } else { count = Int(itemsArray[itemIndex].quantity - 1) } itemsArray[itemIndex].quantity = Int32(count) //Xcode insisted on Int32 } // ... }
对应视图层实现
struct SecondView: View { @ObservedObject var viewModel: MyViewModel // ... var body: some View { // ... ForEach (viewModel.itemsArray, id: \.self) { item in let index = viewModel.itemsArray.firstIndex(of: item) HStack { Button(action: {viewModel.decreaseCount(itemIndex: index!)}) { Image(systemName: "minus.circle") .foregroundColor(Color.gray) } Text("\(viewModel.itemsArray[index!].quantity)") Button(action: {viewModel.increaseCount(itemIndex: index!)}) { Image(systemName: "plus.circle") .foregroundColor(Color.black) } } } } }
额外配置
我还在ViewModel所在文件中添加了如下扩展:
extension Item: Identifiable{}
问题现象
目前ViewModel中的打印语句显示数值已正常更新,但UI并未同步刷新。
补充说明:我测试发现如果先删除指定索引的元素再重新插入,UI可以正常更新,想知道Swift中是否有更高效、优雅的实现方式。
问题根因
SwiftUI的@Published仅在属性本身被重新赋值时触发更新。KMM导出到iOS的Item默认是引用类型(class),修改其内部属性不会改变数组存储的引用地址,数组本身没有触发写入事件,ObservableObject感知不到变更,自然不会刷新UI。另外你当前ForEach用id: \.self+运行时查找索引的写法存在两个问题:一是属性变更不会改变self的标识值,SwiftUI会判定对应单元格无需刷新;二是数组动态变化时firstIndex可能返回nil触发强制解包崩溃。
解决方案
方案1:手动触发更新(KMM场景最优,性能最高)
修改元素属性后手动调用objectWillChange.send()强制通知视图刷新,不需要做数组删除/插入/复制操作,性能和Jetpack Compose中修改副本刷新的逻辑一致:
func increaseCount(itemIndex: Int) { let count = itemsArray[itemIndex].quantity + 1 itemsArray[itemIndex].quantity = count objectWillChange.send() // 手动触发更新事件 print("item count is \(count)") } func decreaseCount(itemIndex: Int) { var count: Int if itemsArray[itemIndex].quantity < 2 { count = 1 } else { count = Int(itemsArray[itemIndex].quantity - 1) } itemsArray[itemIndex].quantity = Int32(count) objectWillChange.send() // 手动触发更新事件 }
同时修正ForEach写法,去掉运行时查找索引的逻辑,直接通过枚举拿到索引和元素,避免强制解包崩溃:
ForEach(Array(viewModel.itemsArray.enumerated()), id: \.element.id) { index, item in HStack { Button(action: { viewModel.decreaseCount(itemIndex: index) }) { Image(systemName: "minus.circle") .foregroundColor(Color.gray) } Text("\(item.quantity)") Button(action: { viewModel.increaseCount(itemIndex: index) }) { Image(systemName: "plus.circle") .foregroundColor(Color.black) } } }
方案2:值类型改造(纯Swift场景推荐)
如果Item是纯Swift定义的类型,将其从class改为struct值类型,修改数组元素属性时会触发数组的写时复制机制,相当于给数组重新赋值,@Published会自动感知变更,不需要手动调用发送更新事件。但KMM跨端导出的模型类默认是引用类型,该方案不适用。
方案3:数组整体赋值
修改完元素属性后给数组重新赋值,也能触发@Published更新,本质和你之前删除再插入的逻辑一致,写法更简洁,但大数组场景下会触发全量差异比对,性能弱于方案1:
func increaseCount(itemIndex: Int) { var tempArray = itemsArray tempArray[itemIndex].quantity += 1 itemsArray = tempArray }
内容的提问来源于stack exchange,提问作者MrBE

