Skip to content

Instantly share code, notes, and snippets.

@SmartJSONEditor
Last active March 31, 2024 05:38
Show Gist options
  • Save SmartJSONEditor/33d796e81554205fd491b6449376c426 to your computer and use it in GitHub Desktop.
Save SmartJSONEditor/33d796e81554205fd491b6449376c426 to your computer and use it in GitHub Desktop.
Observation withObservationTracking as Combine publisher
import Combine
import Foundation
import Observation
// A: Using specific class
/// Class that wraps withObservationTracking function into a Combine compatible continuos stream publisher
public class ObservationTracker<O: Observable & AnyObject, T> {
/// Subscribe to a publisher.
public var valuePublisher: AnyPublisher<T, Never> {
subject.eraseToAnyPublisher()
}
private var subject = PassthroughSubject<T, Never>()
/// Public init, init with Observable object and its keyPath the publisher should post value changes.
public init(_ object: O, keyPath: KeyPath<O, T>) {
scheduleObservationTracking(object: object, keyPath: keyPath)
}
private func scheduleObservationTracking(object: O, keyPath: KeyPath<O, T>) {
_ = withObservationTracking { [weak mainObject = object] in
mainObject?[keyPath: keyPath]
} onChange: {
Task { @MainActor [weak self, weak mainObject = object] in
guard let self = self else { return }
guard let strongObject = mainObject else { return }
self.subject.send(strongObject[keyPath: keyPath])
self.scheduleObservationTracking(object: strongObject, keyPath: keyPath)
}
}
}
}
// B: using Custom publisher
extension Observable {
public func publisher<O: Observable & AnyObject, T>(keyPath: KeyPath<O, T>) -> AnyPublisher<T, Never> {
return ObservablePublisher(object: self as! O, keyPath: keyPath).eraseToAnyPublisher()
}
}
struct ObservablePublisher<Output, Object: Observable & AnyObject>: Publisher {
typealias Failure = Never
var object: Object
var keyPath: KeyPath<Object, Output>
func receive<S: Subscriber>(
subscriber: S
) where S.Input == Output, S.Failure == Never {
let subscription = Subscription(publisher: self, target: subscriber)
subscriber.receive(subscription: subscription)
}
}
private extension ObservablePublisher {
class Subscription<Target: Subscriber>: Combine.Subscription
where Target.Input == Output {
private let publisher: ObservablePublisher
private var target: Target?
init(publisher: ObservablePublisher, target: Target) {
self.publisher = publisher
self.target = target
}
func request(_ demand: Subscribers.Demand) {
if let target = target {
scheduleObservationTracking(target: target, object: publisher.object, keyPath: publisher.keyPath)
}
}
private func scheduleObservationTracking(target: Target, object: Object, keyPath: KeyPath<Object, Output>) {
_ = withObservationTracking { [weak mainObject = object] in
mainObject?[keyPath: keyPath]
} onChange: {
Task { @MainActor [weak self, weak mainObject = object] in
guard let self = self else { return }
guard let strongObject = mainObject else { return }
_ = target.receive(strongObject[keyPath: keyPath])
self.scheduleObservationTracking(target: target, object: strongObject, keyPath: keyPath)
}
}
}
func cancel() {
target = nil
}
}
}
@lukaszciastko
Copy link

Hi. I tried to use this in a ViewModel with SwiftUI, but the view keeps redrawing continuously - any idea why?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment