Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active June 26, 2024 07:38
Show Gist options
  • Save IanKeen/b0048a26dfa0d39ed600a3d46eeae6a9 to your computer and use it in GitHub Desktop.
Save IanKeen/b0048a26dfa0d39ed600a3d46eeae6a9 to your computer and use it in GitHub Desktop.
ObservableObjectContainer: Consolidate nested/child ObservableObjects into a single publisher, useful for 'parent' ObservableObjects
// Setup
class Child: ObservableObject {
@Published var value = ""
}
class Parent: ObservableObjectContainer {
let single = Child()
let array = [Child(), Child()]
func updateSingle() {
single.value = "hello world"
}
func updateArray() {
array[1].value = "hello world"
}
}
// Usage
let parent = Parent()
_ = parent.objectWillChange.sink {
// called if either single or array @Published properties are updated
}
parent.updateSingle()
parent.updateArray()
open class ObservableObjectContainer: ObservableObject {
private var childSubscription: AnyCancellable?
public init() {
bindToChildPublishers()
}
private func childPublishers() -> AnyPublisher<Void, Never> {
let mirror = Mirror(reflecting: self)
func publisher<T>(_ value: T) -> [ObservableObjectPublisher]? {
if let result = (Proxy<T>() as? ObservableObjectProxy)?.extractObjectWillChange(value) {
return [result]
} else if let result = (Proxy<T>() as? ObservableObjectArrayProxy)?.extractObjectWillChange(value) {
return result
} else {
return nil
}
}
let publishers = mirror.children.compactMap {
_openExistential($0.value, do: publisher)
}
return Publishers.MergeMany(publishers.flatMap({ $0 })).eraseToAnyPublisher()
}
func bindToChildPublishers() {
childSubscription = childPublishers().sink(receiveValue: { [weak self] in self?.objectWillChange.send() })
}
}
private struct Proxy<Base> {
func extract<A, B, C>(_ instance: A, _ extract: (Base) -> B) -> C {
return extract(instance as! Base) as! C
}
}
private protocol ObservableObjectProxy {
func extractObjectWillChange<T>(_ instance: T) -> ObservableObjectPublisher
}
extension Proxy: ObservableObjectProxy where Base: ObservableObject, Base.ObjectWillChangePublisher == ObservableObjectPublisher {
func extractObjectWillChange<T>(_ instance: T) -> ObservableObjectPublisher {
extract(instance) { $0.objectWillChange }
}
}
private protocol ObservableObjectArrayProxy {
func extractObjectWillChange<T>(_ instance: T) -> [ObservableObjectPublisher]
}
extension Proxy: ObservableObjectArrayProxy where Base: Sequence, Base.Element: ObservableObject, Base.Element.ObjectWillChangePublisher == ObservableObjectPublisher {
func extractObjectWillChange<T>(_ instance: T) -> [ObservableObjectPublisher] {
extract(instance) { $0.map(\.objectWillChange) }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment