使用Combine监听数组中对象属性变化的实现方式是否正确?
我正在开发一套监听对象变化的实现方案,当前采用以下Swift代码开发。需求是当OfferItem中的isSelected布尔属性发生变化时更新UI,想咨询这种监听数组内对象属性变化的方式是否正确。
class OfferDetail: ObservableObject { @Published var items: [OfferItem] private var anyCancellables = Set<AnyCancellable>() init(deliveryAddress: String, deliveryDate: String, offerItems: [OfferItem]) { self.items = offerItems for item in items { item.$isSelected .sink(receiveValue: { _ in print("OfferDetail Update 1") self.objectWillChange.send() }) .store(in: &anyCancellables) } } } class OfferItem: Identifiable, ObservableObject { @Published var isSelected: Bool = false let uuid = UUID() init() { print(uuid) } } class ViewModel: ObservableObject { @Published var offer: OfferDetail? private var anyCancellables = Set<AnyCancellable>() init() { offer = OfferDetail(deliveryAddress: "", deliveryDate: "", offerItems: [OfferItem(), OfferItem()]) offer?.$items.sink { items in print("ViewModel Update 2") for item in items { print(item.uuid, item.isSelected) } }.store(in: &anyCancellables) } } let viewModel = ViewModel() viewModel.offer?.items.first?.isSelected = true
你的当前实现存在几个关键问题,无法完全满足需求:
仅监听初始化时的数组元素:你在
OfferDetail的init方法里遍历初始的items并订阅它们的isSelected变化,但如果后续数组发生增删(比如添加新的OfferItem、移除已有元素),新元素的isSelected变化不会被监听,而被移除的元素的订阅也不会被取消,会导致内存泄漏。ViewModel中的$items订阅不会触发:OfferDetail里调用objectWillChange.send()只会通知OfferDetail自身的变化,但ViewModel订阅的是offer?.$items——只有当items数组本身被替换(比如重新赋值一个新数组)时,这个订阅才会触发,单个元素的属性变化不会触发$items的发布。
正确的实现方式
方案1:让OfferItem成为值类型(struct)
如果业务允许,将OfferItem改为struct而非class,因为值类型的属性变化会让数组元素整体变化,结合@Published可以自动触发更新:
struct OfferItem: Identifiable { var isSelected: Bool = false let id = UUID() init() { print(id) } } class OfferDetail: ObservableObject { @Published var items: [OfferItem] init(deliveryAddress: String, deliveryDate: String, offerItems: [OfferItem]) { self.items = offerItems } } class ViewModel: ObservableObject { @Published var offer: OfferDetail? private var anyCancellables = Set<AnyCancellable>() init() { offer = OfferDetail(deliveryAddress: "", deliveryDate: "", offerItems: [OfferItem(), OfferItem()]) offer?.$items.sink { items in print("ViewModel Update 2") for item in items { print(item.id, item.isSelected) } }.store(in: &anyCancellables) } } let viewModel = ViewModel() viewModel.offer?.items[0].isSelected = true
这种方式最简单,值类型的特性会自动帮你处理元素属性变化的通知,无需手动订阅每个元素。
方案2:保持class类型,动态监听数组元素变化
如果必须使用class类型的OfferItem,需要监听数组的变化,动态添加/移除元素的订阅:
class OfferDetail: ObservableObject { @Published var items: [OfferItem] { didSet { // 数组变化时,取消旧元素的订阅,订阅新元素 cancelSubscriptions() subscribeToItems() } } private var anyCancellables = Set<AnyCancellable>() init(deliveryAddress: String, deliveryDate: String, offerItems: [OfferItem]) { self.items = offerItems subscribeToItems() } private func subscribeToItems() { for item in items { item.$isSelected .sink(receiveValue: { [weak self] _ in print("OfferDetail Update 1") self?.objectWillChange.send() }) .store(in: &anyCancellables) } } private func cancelSubscriptions() { anyCancellables.removeAll() } } class OfferItem: Identifiable, ObservableObject { @Published var isSelected: Bool = false let id = UUID() init() { print(id) } } class ViewModel: ObservableObject { @Published var offer: OfferDetail? private var anyCancellables = Set<AnyCancellable>() init() { offer = OfferDetail(deliveryAddress: "", deliveryDate: "", offerItems: [OfferItem(), OfferItem()]) // 订阅OfferDetail的objectWillChange,而非$items offer?.objectWillChange .sink { [weak self] in print("ViewModel Update 2") self?.offer?.items.forEach { item in print(item.id, item.isSelected) } } .store(in: &anyCancellables) } } let viewModel = ViewModel() viewModel.offer?.items.first?.isSelected = true
这里做了两个关键修改:
- 在
OfferDetail的items属性添加didSet,数组变化时重新订阅所有元素 ViewModel订阅OfferDetail的objectWillChange,这样每当OfferItem的isSelected变化时,OfferDetail发送的通知会被ViewModel接收
总结
如果没有特殊业务要求,优先选择方案1(值类型struct),代码更简洁且不易出错;如果必须使用引用类型,则采用方案2来动态管理元素的订阅,避免内存泄漏和监听失效。
内容的提问来源于stack exchange,提问作者Aravind Bhuvanendran

