You can't actually be caring about each property individually, if they're all getting erased to AnyCancellable
s. Instead, forward them through another object.
import Combine
public extension Published
where Value: ObservableObject, Value.ObjectWillChangePublisher == ObservableObjectPublisher {
/// An `ObservableObject` that forwards its `objectWillChange` through a parent.
@propertyWrapper struct Object {
// MARK: propertyWrapper
@available(*, unavailable, message: "The enclosing type is not an 'ObservableObject'.")
public var wrappedValue: Value { get { fatalError() } set { } }
public static subscript<Parent: ObservableObject>(
_enclosingInstance parent: Parent,
wrapped _: ReferenceWritableKeyPath<Parent, Value>,
storage keyPath: ReferenceWritableKeyPath<Parent, Self>
) -> Value
where Parent.ObjectWillChangePublisher == ObservableObjectPublisher {
get { Self[_enclosingInstance: parent, storage: keyPath] }
set { Self[_enclosingInstance: parent, storage: keyPath] = newValue }
}
// MARK: PublishedObject
public var value: Value
/// The subscription which forwards `_wrappedValue`'s `objectWillChange` through a "parent".
public var objectWillChangeSubscription: AnyCancellable!
}
}
// MARK: - PublishedObject
extension Published.Object: PublishedObject {
public var object: Value { value }
}
// MARK: - public
public extension Published.Object {
init(wrappedValue: Value) {
value = wrappedValue
}
}
import Combine
public protocol PublishedObject {
associatedtype Object: ObservableObject
where Object.ObjectWillChangePublisher == ObservableObjectPublisher
associatedtype WrappedValue
init(wrappedValue: WrappedValue)
var value: WrappedValue { get set }
var object: Object { get }
var objectWillChangeSubscription: AnyCancellable! { get set }
}
extension PublishedObject {
static subscript<Parent: ObservableObject>(
_enclosingInstance parent: Parent,
storage keyPath: ReferenceWritableKeyPath<Parent, Self>
) -> WrappedValue
where Parent.ObjectWillChangePublisher == ObservableObjectPublisher {
get {
@Computed(root: parent, keyPath: keyPath) var `self`
// It's `nil` until a parent can be provided.
if self.objectWillChangeSubscription == nil {
self.setParent(parent)
}
return self.value
}
set {
@Computed(root: parent, keyPath: keyPath) var `self`
self.value = newValue
self.setParent(parent)
parent.objectWillChange.send()
}
}
private mutating func setParent<Parent: ObservableObject>(_ parent: Parent)
where Parent.ObjectWillChangePublisher == ObservableObjectPublisher {
objectWillChangeSubscription = object.objectWillChange.subscribe(parent.objectWillChange)
}
}
/// A workaround for limitations of Swift's computed properties.
///
/// Limitations of Swift's computed property accessors:
/// 1. They are not mutable.
/// 2. They cannot be referenced as closures.
@propertyWrapper public struct Computed<Value> {
public typealias Get = () -> Value
public typealias Set = (Value) -> Void
public init(
get: @escaping Get,
set: @escaping Set
) {
self.get = get
self.set = set
}
public var get: Get
public var set: Set
public var wrappedValue: Value {
get { get() }
nonmutating set { set(newValue) }
}
public var projectedValue: Self {
get { self }
set { self = newValue }
}
}
// MARK: - public
public extension Computed {
/// Convert a `KeyPath` to a get/set accessor pair.
init<Root>(
root: Root,
keyPath: ReferenceWritableKeyPath<Root, Value>
) {
self.init(
get: { root[keyPath: keyPath] },
set: { root[keyPath: keyPath] = $0 }
)
}
}
import Combine
/// The simplest way to forward one `objectWillChange` through another
/// is to make `ObservableObjectPublisher` be a `Subject`,
/// so that `Publisher.subscribe` can be used with it.
extension ObservableObjectPublisher: Subject {
public func send(subscription: any Subscription) {
subscription.request(.unlimited)
}
public func send(_: Void) {
send()
}
public func send(completion _: Subscribers.Completion<Never>) { }
}