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

如何在属性包装器任意位置获取外部ObservableObject实例调用objectWillChange.send()

解决@PublishedMutable通过publisher修改时触发objectWillChange的问题

问题描述

我需要实现一个类似Combine框架中@Published的属性包装器@PublishedMutable,支持通过projectedValue中的publisher发送值来修改包装属性。目前直接修改属性和通过publisher发送值都能正常修改属性值,但只有直接修改属性时会调用objectWillChange.send(),通过publisher修改时不会触发。

原属性包装器代码

@propertyWrapper
struct PublishedMutable<Value> {
    static subscript<T: ObservableObject>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Value {
        get {
            instance[keyPath: storageKeyPath].storage
        }
        set {
            let publisher = instance.objectWillChange
            // 仅演示用的不安全假设,生产环境不可用
            (publisher as? ObservableObjectPublisher)?.send()
            
            instance[keyPath: storageKeyPath].storage = newValue
        }
    }
    
    @available(*, unavailable,
                message: "@PublishedMutable can only be applied to classes"
    )
    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }
    
    private var storage: Value{
        get { publisher.value }
        set { publisher.send(newValue) }
    }
    typealias Publisher = CurrentValueSubject<Value, Never>

    var projectedValue: Publisher { self.publisher }

    private var publisher: Publisher
    init(initialValue: Value) {
        self.publisher = Publisher(initialValue)
    }
    
    init(wrappedValue: Value) {
        self.init(initialValue: wrappedValue)
    }
}

测试代码

class FooClass: ObservableObject {
    @PublishedMutable var currentPageContent = "foo"
    
    private var cancellables: Set<AnyCancellable> = []
    
    func connect() {
        $currentPageContent.sink { newPage in
            print(newPage)
        }
        .store(in: &cancellables)
        objectWillChange.sink { _ in
            print("Update")
        }
        .store(in: &cancellables)
    }
}

let fooClass = FooClass()
DispatchQueue.main.async {
    fooClass.connect()
    
    fooClass.currentPageContent = "bar"
    
    fooClass.$currentPageContent.send("foobar")
}

PlaygroundPage.current.needsIndefiniteExecution = true

当前输出

foo
Update
bar
foobar

预期输出

foo
Update
bar
Update
foobar

解决方案

核心思路是让属性包装器持有外部ObservableObject实例的弱引用,并在publisher发送新值时触发objectWillChange.send()。具体修改如下:

  1. 添加弱引用存储外部实例,并通过静态下标在首次访问时捕获该实例
  2. 订阅publisher的事件,每次收到新值时触发外部实例的objectWillChange
  3. 直接修改属性时,简化objectWillChange的调用逻辑(无需类型转换)

修改后的属性包装器代码

@propertyWrapper
struct PublishedMutable<Value> {
    static subscript<T: ObservableObject>(
        _enclosingInstance instance: T,
        wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Value>,
        storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
    ) -> Value {
        get {
            let wrapper = instance[keyPath: storageKeyPath]
            // 首次访问时捕获外部实例并订阅publisher事件
            if wrapper.enclosingInstance == nil {
                wrapper.enclosingInstance = instance
                wrapper.publisher.sink { [weak wrapper] _ in
                    wrapper?.enclosingInstance?.objectWillChange.send()
                }.store(in: &wrapper.cancellables)
            }
            return wrapper.storage
        }
        set {
            // 直接触发objectWillChange,无需类型转换
            instance.objectWillChange.send()
            instance[keyPath: storageKeyPath].storage = newValue
        }
    }
    
    @available(*, unavailable, message: "@PublishedMutable can only be applied to classes")
    var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }
    
    // 存储外部ObservableObject实例的弱引用
    private weak var enclosingInstance: (any ObservableObject)?
    // 存储publisher的订阅对象,避免提前释放
    private var cancellables = Set<AnyCancellable>()
    
    private var storage: Value{
        get { publisher.value }
        set { publisher.send(newValue) }
    }
    
    typealias Publisher = CurrentValueSubject<Value, Never>
    var projectedValue: Publisher { publisher }
    private var publisher: Publisher
    
    init(initialValue: Value) {
        self.publisher = Publisher(initialValue)
    }
    
    init(wrappedValue: Value) {
        self.init(initialValue: wrappedValue)
    }
}

关键修改说明

  • 弱引用捕获外部实例:在静态下标get方法中,首次访问包装属性时将外部ObservableObject实例赋值给enclosingInstance弱引用,避免循环引用。
  • 订阅publisher事件:通过订阅publisher的sink方法,每次收到新值时调用外部实例的objectWillChange.send(),确保通过$foo.send()修改值时也能触发变更通知。
  • 简化直接修改逻辑:直接调用instance.objectWillChange.send(),无需将其强制转换为ObservableObjectPublisher,提升代码安全性。

运行修改后的代码后,测试输出将符合预期,每次属性值变更(无论是直接修改还是通过publisher发送)都会触发Update打印。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 15:30:04