Dynamically accessing `.objectWillChange`

I'm trying to implement something similar to @Published, only that it will also trigger objectWillChange.send() if the published value is itself also an ObservableObject. In order to do so, I need to dynamically access the published value's .objectWillChange in order to subscribe to it, and re-emit its events upstream.

Since ObservableObject is a PAT, a naïve casting won't do, and since the default implementation of .objectWillChange is implemented in a protocol extension, using Mirror to search for a publisher doesn't seem to work wither.

Can it be done?

I think that it can - with some restriction:
You can add a property wrapper that knows enclosing self and restrict it to where you have an ObservableObject where it’s associated willChange publisher is an ObservableObjectPublisher. Because that one has a ‘send’ method.
So it works in case you do not add a custom willChange publisher. Just like @Published.

Not at a computer, so I can’t verify the details, but something like that. :blush:

Sorry, I misread that you are trying to get at the wrapped value’s objectWillChange… I can’t directly tell if generic restrictions could help in that case…

Add an init(wrappedValue:) overload in an extension that requires Value: ObservableObject:

import Combine

@propertyWrapper
public struct ObservablePublished<Value> {

    private let valueWillChange: AnyPublisher<Void, Never>
    private let value: Value

    public init(wrappedValue: Value) {
        print("non-ObservableObject init with \(wrappedValue)")
        self.value = wrappedValue
        valueWillChange = Empty(completeImmediately: false).eraseToAnyPublisher()
    }

    @available(*, unavailable, message: "@Published is only available on properties of classes")
    public var wrappedValue: Value {
        get { fatalError() }
        set { fatalError() }
    }

    public static subscript<EnclosingSelf>(_enclosingInstance object: EnclosingSelf, wrapped wrappedKeyPath: Swift.ReferenceWritableKeyPath<EnclosingSelf, Value>, storage storageKeyPath: Swift.ReferenceWritableKeyPath<EnclosingSelf, ObservablePublished<Value>>) -> Value where EnclosingSelf : AnyObject {
        get { object[keyPath: storageKeyPath].value }
        set { }
    }
}

extension ObservablePublished where Value: ObservableObject {
    public init(wrappedValue: Value) {
        print("ObservableObject init with \(wrappedValue)")
        self.value = wrappedValue
        valueWillChange = wrappedValue.objectWillChange.map { _ in () }.eraseToAnyPublisher()
    }
}

class MyInnerObservable: ObservableObject { }

class MyOuterObservable: ObservableObject {
    @ObservablePublished
    var nonObservable: Int = 123

    @ObservablePublished
    var observable: MyInnerObservable = .init()
}

let outer = MyOuterObservable()
1 Like

Check out Open Combine's implementation of Observable Object and its synthesised @Published, maybe it could give you a few pointers.

Nice trick!

But how do i resubscribe on value.didSet?

Actually you can check PAT dynamically. See this thread.

import Combine

struct Dispatch<Model> {
    func apply<A, R0, R1>(_ a: A, _ f: (Model) -> R0) -> R1 {
        f(a as! Model) as! R1
    }
}

protocol ObservableObjectDispatch {
    func getObjectWillChange<T>(_ x: T) -> AnyPublisher<Void, Never>
}

extension Dispatch: ObservableObjectDispatch where Model: ObservableObject {
    func getObjectWillChange<T>(_ x: T) -> AnyPublisher<Void, Never> {
        apply(x) { $0.objectWillChange.eraseToAnyPublisher() }
    }
}

func getObjectWillChangeIfObservable<T>(_ x: T) -> AnyPublisher<Void, Never>? {
    (Dispatch<T>() as? ObservableObjectDispatch)?.getObjectWillChange(x)
}

// test
class MyObservable: ObservableObject {}

let x = MyObservable()
getObjectWillChangeIfObservable(x) // ok
getObjectWillChangeIfObservable("not observable") // nil

// for Any type, you also need _openExistential
let y: Any = MyObservable()
getObjectWillChangeIfObservable(y) // nil
_openExistential(y, do: getObjectWillChangeIfObservable) // ok
1 Like