如何在属性包装器任意位置获取外部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()。具体修改如下:
- 添加弱引用存储外部实例,并通过静态下标在首次访问时捕获该实例
- 订阅publisher的事件,每次收到新值时触发外部实例的
objectWillChange - 直接修改属性时,简化
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
相关产品推荐
相关产品推荐

