Skip to content

Instantly share code, notes, and snippets.

@marcoarment
Last active January 18, 2024 06:01
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marcoarment/88ee13df326c25c1f0a68c09e086f03f to your computer and use it in GitHub Desktop.
Save marcoarment/88ee13df326c25c1f0a68c09e086f03f to your computer and use it in GitHub Desktop.
A Combine publisher for `@Observable` key-paths
// I think this works and doesn't create circular references?
/*
Usage: Add OCObservableKeyPaths conformance to the observed type. Then, e.g.
audioPlayer
.publisher(forObservableKeyPaths: [ \.timestamp, \.duration ])
.sink {
// respond to change
}
*/
import Foundation
import Observation
import Combine
public protocol OCObservableKeyPaths: AnyObject, Observable { }
extension OCObservableKeyPaths {
public func publisher(forObservableKeyPaths: [PartialKeyPath<Self>]) -> OCObservationPublisher<Self> {
OCObservationPublisher(object: self, keyPaths: forObservableKeyPaths)
}
}
// Thanks to https://www.swiftbysundell.com/articles/building-custom-combine-publishers-in-swift/
public struct OCObservationPublisher<T: OCObservableKeyPaths>: Publisher {
public typealias Output = Void
public typealias Failure = Never
fileprivate var object: T
fileprivate var keyPaths: [PartialKeyPath<T>]
public func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, Void == S.Input {
subscriber.receive(subscription: OCObservationSubscription<S>(target: subscriber, object: object, keyPaths: keyPaths))
}
}
fileprivate class OCObservationSubscription<Target: Subscriber>: Subscription where Target.Input == Void {
private var target: Target?
private let touchKeyPaths: (() -> Void)
func request(_ demand: Subscribers.Demand) {}
func cancel() { target = nil }
init<T: OCObservableKeyPaths>(target: Target?, object: T, keyPaths: [PartialKeyPath<T>]) {
self.target = target
touchKeyPaths = { [weak object] in
guard let object else { return }
for keyPath in keyPaths {
_ = object[keyPath: keyPath]
}
}
performAction(true)
}
@objc private func performAction(_ isInitialCall: Bool) {
if !isInitialCall { _ = self.target?.receive() }
withObservationTracking {
touchKeyPaths()
} onChange: { [weak self] in
guard let self else { return }
RunLoop.main.cancelPerform(#selector(performAction), target: self, argument: false)
RunLoop.main.perform(#selector(performAction), target: self, argument: false, order: 0, modes: [.common])
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment