Skip to content

Instantly share code, notes, and snippets.

@alberussoftware
Forked from rnapier/observable.swift
Last active December 26, 2020 00:22
Show Gist options
  • Save alberussoftware/086b92e85230c77509326f2bac623769 to your computer and use it in GitHub Desktop.
Save alberussoftware/086b92e85230c77509326f2bac623769 to your computer and use it in GitHub Desktop.
New Observable idea
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