You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI+Combine中Store-ViewModel模式的状态同步方案咨询

问题解析与解决方案

首先明确几个核心疑问:

  • 这不属于传统委托模式,而是基于Combine的发布-订阅模式的补充方案——委托是一对一的回调,而这里是一对多的全局变更通知,更贴合响应式架构的设计思路。
  • 它可以看作是对@Published/ObservableObject的场景化扩展:因为@Published仅监听属性本身的赋值操作(比如把整个Library实例替换掉),无法感知引用类型模型的内部属性变化,或者值类型模型的局部修改(虽然值类型修改会生成新实例,但频繁局部修改会导致多次通知),你的方案就是为了填补这个感知空白。

针对你当前方案的性能问题(多次触发sink),下面分两种场景给出优化方案:


场景1:模型是值类型(比如你的Library是struct)

值类型的特性是局部修改会生成新实例,所以如果你的LocalLibraryStore中频繁修改library的内部属性(比如多次操作books字典),会多次触发didSet和publish(),导致changed发布多次事件。

优化方案:

  1. 让Store批量处理模型修改,确保一次逻辑变更只生成一个新实例,从而只发送一次通知。
  2. 将PassthroughSubject<Any, Never>改为PassthroughSubject<Void, Never>——我们只需要通知"发生了变化",不需要传递具体值,语义更清晰。
  3. 在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完全一致,无需额外修改。

额外建议

  1. 不要将VM的objectWillChange设为可写属性——ObservableObject协议已经定义了只读的objectWillChange,强行开放写入会破坏封装性。
  2. VM中无需用@Published修饰Store实例:Store是引用类型,@Published仅监听实例替换,对内部变更无感知,反而会增加不必要的复杂度。
  3. 用[weak self]代替[unowned self]:避免VM被意外释放时出现野指针问题,这是Combine订阅的最佳实践。

内容的提问来源于stack exchange,提问作者Beginner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 12:47:34