SwiftUI+Combine中Store-ViewModel模式的状态同步方案咨询
问题解析与解决方案
首先明确几个核心疑问:
- 这不属于传统委托模式,而是基于Combine的发布-订阅模式的补充方案——委托是一对一的回调,而这里是一对多的全局变更通知,更贴合响应式架构的设计思路。
- 它可以看作是对
@Published/ObservableObject的场景化扩展:因为@Published仅监听属性本身的赋值操作(比如把整个Library实例替换掉),无法感知引用类型模型的内部属性变化,或者值类型模型的局部修改(虽然值类型修改会生成新实例,但频繁局部修改会导致多次通知),你的方案就是为了填补这个感知空白。
针对你当前方案的性能问题(多次触发sink),下面分两种场景给出优化方案:
场景1:模型是值类型(比如你的Library是struct)
值类型的特性是局部修改会生成新实例,所以如果你的LocalLibraryStore中频繁修改library的内部属性(比如多次操作books字典),会多次触发didSet和publish(),导致changed发布多次事件。
优化方案:
- 让Store批量处理模型修改,确保一次逻辑变更只生成一个新实例,从而只发送一次通知。
- 将
PassthroughSubject<Any, Never>改为PassthroughSubject<Void, Never>——我们只需要通知"发生了变化",不需要传递具体值,语义更清晰。 - 在VM侧用节流操作合并短时间内的冗余通知。
改进后代码:
Store端
struct Library { var books: [String: Book] = [:] // 假设Book是你的书籍模型 } protocol Store { var changed: PassthroughSubject<Void, Never> { get } } protocol LibraryStore: Store { var library: Library { get } func removeBook(withId id: String) } class LocalLibraryStore: LibraryStore { private(set) var library: Library { didSet { publish() } } let changed = PassthroughSubject<Void, Never>() private var subscriptions = Set<AnyCancellable>() init(initialLibrary: Library = Library()) { self.library = initialLibrary } func removeBook(withId id: String) { // 批量修改:先创建副本修改所有内容,再一次性赋值 var updatedLibrary = library updatedLibrary.books.removeValue(forKey: id) self.library = updatedLibrary } private func publish() { changed.send() print("仅在模型整体更新时触发一次") } }
VM端
protocol VM: ObservableObject { var subscriptions: Set<AnyCancellable> { get set } } extension VM { func republishChanges(of store: Store) { store.changed // 节流10ms,合并短时间内的多次通知 .throttle(for: .milliseconds(10), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in print("VM仅接收合并后的变更通知") self?.objectWillChange.send() } .store(in: &subscriptions) } } class BadgeVM: VM { let specificStore: LibraryStore var subscriptions = Set<AnyCancellable>() init(store: LibraryStore) { self.specificStore = store republishChanges(of: store) } var totalBooks: Int { specificStore.library.books.count } } class OtherVM: VM { let specificStore: LibraryStore var subscriptions = Set<AnyCancellable>() init(store: LibraryStore) { self.specificStore = store republishChanges(of: store) } var isBookVeryExpensive: Bool { // 基于library的计算逻辑 specificStore.library.books.values.contains { $0.price > 100 } } func bookMysteriouslyDisappears() { specificStore.removeBook(withId: "some-book-id") } }
场景2:模型是引用类型(比如Library改为class)
如果你的模型是引用类型,直接让它遵循ObservableObject,Store监听模型的内部变更并转发,这样最贴合Combine的原生设计。
改进后代码:
模型与Store端
class Library: ObservableObject { @Published var books: [String: Book] = [:] } protocol Store { var changed: PassthroughSubject<Void, Never> { get } } protocol LibraryStore: Store { var library: Library { get } func removeBook(withId id: String) } class LocalLibraryStore: LibraryStore { private(set) var library: Library let changed = PassthroughSubject<Void, Never>() private var subscriptions = Set<AnyCancellable>() init(library: Library = Library()) { self.library = library // 监听模型的内部变更,自动转发到Store的changed发布者 library.objectWillChange .map { _ in () } .subscribe(changed) .store(in: &subscriptions) } func removeBook(withId id: String) { // 直接修改模型内部属性,模型会自动触发objectWillChange library.books.removeValue(forKey: id) } }
VM端代码和场景1完全一致,无需额外修改。
额外建议
- 不要将VM的
objectWillChange设为可写属性——ObservableObject协议已经定义了只读的objectWillChange,强行开放写入会破坏封装性。 - VM中无需用
@Published修饰Store实例:Store是引用类型,@Published仅监听实例替换,对内部变更无感知,反而会增加不必要的复杂度。 - 用
[weak self]代替[unowned self]:避免VM被意外释放时出现野指针问题,这是Combine订阅的最佳实践。
内容的提问来源于stack exchange,提问作者Beginner
相关产品推荐
相关产品推荐

