I wrote a property wrapper named @Republished
to encapsulate this pattern. To use it, you'd write the outer class like so:
final class AggregatedController: ObservableObject {
@Republished var stringController: StringController
@Republished var intController: IntController
init() {
self.stringController = .init()
self.intController = .init()
}
}
Notes on my code:
-
It's largely untested. I wrote this for fun and never used it in production.
-
It uses the unofficial "enclosing self" feature of property wrappers
-
Keep in mind this note from Philippe Hausler that performance may degrade if your root class has no
@Published
property at all (and no customobjectWillChange
publisher). I think is because in this case Combine stores the object's publisher in some sort of global lookup table.else wise you go down a pretty non-performant path to accomoxdate
ObservbleObject
types that don't have any@Published
properties
import Combine
@propertyWrapper
struct Republished<Obj: ObservableObject> {
private var storage: Obj
private var subscription: AnyCancellable? = nil
init(wrappedValue: Obj) {
self.storage = wrappedValue
}
@available(*, unavailable, message: "Republished can only be used inside reference types that conform to ObservableObject")
var wrappedValue: Obj {
get { fatalError() }
set { fatalError() }
}
static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance enclosing: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Obj>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Republished<Obj>>
) -> Obj where EnclosingSelf.ObjectWillChangePublisher == ObservableObjectPublisher {
get {
// Connect child's objectWillChange to parent's objectWillChange.
if enclosing[keyPath: storageKeyPath].subscription == nil {
let parentPublisher = enclosing.objectWillChange
let childPublisher = enclosing[keyPath: storageKeyPath].storage.objectWillChange
enclosing[keyPath: storageKeyPath].subscription = childPublisher.sink { _ in
parentPublisher.send()
}
}
return enclosing[keyPath: storageKeyPath].storage
}
set {
// Cancel old child's connection to parent.
if enclosing[keyPath: storageKeyPath].subscription != nil {
enclosing[keyPath: storageKeyPath].subscription = nil
}
enclosing[keyPath: storageKeyPath].storage = newValue
// Connect new child's objectWillChange to parent's objectWillChange.
let parentPublisher = enclosing.objectWillChange
let childPublisher = newValue.objectWillChange
enclosing[keyPath: storageKeyPath].subscription = childPublisher.sink { _ in
parentPublisher.send()
}
// Must tell parent explicitly that it has changed.
parentPublisher.send()
}
}
}