sveinhal
(Svein Halvor Halvorsen)
1
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. 
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…
mayoff
(Rob Mayoff)
4
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.
sveinhal
(Svein Halvor Halvorsen)
6
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