-
-
Save alberussoftware/086b92e85230c77509326f2bac623769 to your computer and use it in GitHub Desktop.
New Observable idea
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import struct Foundation.UUID | |
final class Disposable { | |
let dispose: () -> Void | |
init(dispose: @escaping () -> Void) { | |
self.dispose = dispose | |
} | |
deinit { | |
dispose() | |
} | |
} | |
final class DisposeBag { | |
private var disposables = [Disposable]() | |
func insert(_ disposable: Disposable) { | |
disposables.append(disposable) | |
} | |
} | |
extension Disposable { | |
func disposed(by bag: DisposeBag) { | |
bag.insert(self) | |
} | |
} | |
/// An `Observable` wraps any value. If you add an observer handler, then every time the value is set, your handler will be | |
/// called with the new value. Adding an observer returns a closure that is used to remove the observer. Note that the handler | |
/// is called every time the value is set, even if this does not change the value. If you only want the handler to be called | |
/// when the value changes, see `CoalescingObservable`. | |
class Observable<T> { | |
private var isTrigger = true | |
var value: T { didSet { if isTrigger { notifyAllObservers(oldValue: oldValue) } } } | |
fileprivate var observers: [UUID: (_ new: T, _ old: T?) -> Void] = [:] | |
/// Adds an observer handler, returning a closure that will remove the observer | |
func addObserver(options: ObservableOptions = [], didChange: @escaping (_ new: T, _ old: T?) -> Void) -> Disposable { | |
let identifier = UUID() | |
observers[identifier] = didChange | |
if options.contains(.initial) { didChange(value, nil) } | |
return .init { [weak self] in self?.observers[identifier] = nil } | |
} | |
init(_ value: T) { | |
self.value = value | |
} | |
/// Generally used internally, but may be used to "prime" all observers with the current value. | |
func notifyAllObservers(oldValue: T? = nil) { | |
observers.values.forEach { $0(value, oldValue) } | |
} | |
@discardableResult | |
func writeWithoutTrigger<V>(_ body: (inout T) throws -> V) rethrows -> V? { | |
isTrigger = false; defer { isTrigger = true } | |
return try body(&value) | |
} | |
} | |
struct ObservableOptions: OptionSet { | |
let rawValue: Int | |
static let initial = ObservableOptions(rawValue: 1 << 0) | |
} | |
extension Observable: CustomStringConvertible { | |
var description: String { "\(value)" } | |
} | |
/// An `Observable` that only fires notifications when the value changes (rather than every time it is set). | |
final class CoalescingObservable<T: Equatable>: Observable<T> { | |
override func notifyAllObservers(oldValue: T?) { | |
guard oldValue != value else { return } | |
observers.values.forEach { $0(value, oldValue) } | |
} | |
} | |
@propertyWrapper | |
struct ObservableProperty<Value> { | |
private let observable: Observable<Value> | |
var wrappedValue: Value { | |
get { observable.value } | |
nonmutating set { observable.value = newValue } | |
} | |
var projectedValue: Observable<Value> { observable } | |
init(wrappedValue: Value) { | |
observable = .init(wrappedValue) | |
self.wrappedValue = wrappedValue | |
} | |
} | |
@propertyWrapper | |
struct CoalescingObservableProperty<Value: Equatable> { | |
private let observable: CoalescingObservable<Value> | |
var wrappedValue: Value { | |
get { observable.value } | |
nonmutating set { observable.value = newValue } | |
} | |
var projectedValue: CoalescingObservable<Value> { observable } | |
init(wrappedValue: Value) { | |
observable = .init(wrappedValue) | |
self.wrappedValue = wrappedValue | |
} | |
} | |
/// By conforming to the `Observer` protocol, it is easier to handle the common case of observing several things at one time, and | |
/// removing all the observations together. Usually this isn't needed since DisposeBag automatically cleans everything on deinit. | |
protocol Observer: AnyObject { | |
var disposeBag: DisposeBag { get set } | |
} | |
extension Observer { | |
/// Adds a new observation, updating the object's list of observations to remove later. */ | |
func observe<T>(_ observable: Observable<T>, options: ObservableOptions = [], didChange: @escaping (_ newValue: T, _ oldValue: T?) -> Void) { | |
observable.addObserver(options: options, didChange: didChange).disposed(by: disposeBag) | |
} | |
/// Removes all observations registered in observerRemovers (by calling `observe` or by adding them directly to the array). | |
func removeAllObservations() { | |
disposeBag = .init() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment