How is the `Published` property wrapper implemented?

More specifically, how is a @Published member of an ObservableObject wired up to its objectWillChange?

I don't know if the magic resides in @Published or in ObservableObject, but the interaction between the two is quite mysterious.

I see that objectWillChange has a default implementation that's available when Self.ObjectWillChangePublisher == ObservableObjectPublisher. Is it doing some kind of reflection to identify all the @Published properties of self to subscribe to them?

(I know Published and ObservableObject are from Combine, which is a closed-source proprietary Apple framework. I hope this question isn't off-topic, I'm just looking to replicate the same structure in my own Swift programs.)

Any insights appreciated!

3 Likes

Using an unofficial property wrapper feature which allows accessing the enclosing class instance on member access (but not during initialisation!). Explained e.g. in Accessing a Swift property wrapper’s enclosing instance | Swift by Sundell.

3 Likes

i can do something like..

@propertyWrapper
struct Published<T> {
    var wrappedValue: T {
        willSet { /*...*/ }
        didSet { /*...*/ }
    }
    init(wrappedValue: T) {
        
        // * -- maybe magic here! -- */
        
        /* or do more specific stuff.. 
        switch (wrappedValue) {
        case is Bool :
            break
        case is Error :
            break
        case is Optional<Error>.Type :
            break
        default: break
        }
        */
        self.wrappedValue = wrappedValue
    }
}

So the question is how to install some ObservableObject mechanism.
Reminder the question comes only up for Xcode projects prior macOS 10.14 (in example in Xcode 11.3.1 where Combine is not available as is)

Ooooo interesting.

Funny enough, I did know about _enclosingInstance, but I didn't think to do something like

let publisher = instance.objectWillChange
// This assumption is definitely not safe to make in
// production code, but it's fine for this demo purpose:
(publisher as! ObservableObjectPublisher).send()

So is ObservableObjectPublisher pretty much just equivalent to PassthroughSubject<(), Never>? It's its own class, not a typealias. Any clue why that might be?

Take a look at OpenCombine for an example implementation, that mirrors the reflection wizardry Apple is using.

1 Like

This seems like a viable, but totally different approach then what @pyrtsa showed (using _enclosingInstance to allow the @Published property to trigger the publisher on set).

Are there any trade-offs between the two?

That, I don't know enough (either) to say. But what I did see is that OpenCombine uses the subscript for something related, but different.

Here's a simple code example where Combine can be compared against OpenCombine. The former runs with no errors, the latter crashes trying to use reflection.

#if true
import Combine
#else
import OpenCombine
#endif

final class Example: ObservableObject, CustomReflectable, CustomPlaygroundDisplayConvertible {
    @Published var number = 0
    var customMirror: Mirror { fatalError() }
    var playgroundDescription: Any { "Example" } // prevent Xcode Playground from using reflection
}

let example = Example()
var cancellables: Set<AnyCancellable> = []
example.$number.sink { print("number: \($0)") }.store(in: &cancellables)
example.objectWillChange.sink { () in print("objectWillChange") }.store(in: &cancellables)
example.number += 1

With Combine, the above prints "number: 0", then "objectWillChange", then "number: 1". With OpenCombine, it crashes after the first line.

I think none of this functionality necessitates the use of reflection.

2 Likes

interestingly your infos lead to some fix that lets me face the next step, almost there.
Xcode 11.3.1, macOS 10.14.6 making indeed use of OpenCombine Framework, all 4 libs linked & import OpenCombine as well as import OpenCombineDispatch declared and then below telling what Published is with typealias Published = OpenCombine.Published. This lets the compiler go one step ahead and shows me

someObservable.$highlighted
    .receive(on: DispatchQueue.main) //<-- Argument type 'DispatchQueue' does not conform to expected type 'Scheduler'
    .sink { [weak self] newValue in
        self?.someView.needsDisplay = true
    }
    .store(in: &cancellables)

typealias Scheduler = OpenCombine.Scheduler does not fix that..
and when i search the source i find above the public struct Published declarations..

#if swift(>=5.1)
...
@available(swift, introduced: 5.1)

which says.. no no my Xcode 11.3.1 with Swift 5 should not do that. but the first easy fix did indeed work, kinda surprised. But the other typealias same way fixing does it not (yet)..

Try this:

    .receive(on: DispatchQueue.main.ocombine)

One thing caught my curiosity:

How can (real) Combine have a default implementation of ObservableObject.objectWillChange with a stable address? Extensions can't add new stored properties to an instance.

import Combine

class Test: ObservableObject {}

let testObject = Test()

// If `objectWillChange` is a computed property, how is it able to return
// the same object on each call? Where is the underlying storage?
print(ObjectIdentifier(testObject.objectWillChange))
print(ObjectIdentifier(testObject.objectWillChange))

I thought perhaps they're using associated objects, so I tossed in a objc_removeAssociatedObjects(testObject) between those two lines, but it still worked.

That led to me hope into the debugger to see what the raw machine code was doing. Turns out that objectWillChange calls a private reflection API of the Swift standard library (_forEachField(of:options:body)). Notably, this reflection mechanism bypasses the CustomReflectable mechanism, which is why your example doesn't crash with real Combine. They're accessing the Published properties, and storing the ObservableObjectPublisher instance within those property wrappers.

From a performance, I think this make sense. The property wrapper _enclosingInstance approach requires a dynamic type-check of self on every setting of a property. In contrast, this reflection approach just does some pricy reflection once up-front on the construction of an ObservableObject, but the main flow (emitting events when Published properties change) can be simpler.

4 Likes

Here's a previous thread (2019) where this was also discussed:

The implementation of objectWillChange is a bit sneaky; there is no associated object. Instead we take advantage of knowing the metadata layout and key paths and access the existing storage from our wrapper. On the surface it seems like an associated object but in reality it is more like a metadata lens.

2 Likes