Skip to content

Instantly share code, notes, and snippets.

@rnapier
Created May 16, 2018 18:51
Show Gist options
  • Select an option

  • Save rnapier/6dff938397d532e5bbfcace3c54f64a6 to your computer and use it in GitHub Desktop.

Select an option

Save rnapier/6dff938397d532e5bbfcace3c54f64a6 to your computer and use it in GitHub Desktop.

Revisions

  1. rnapier created this gist May 16, 2018.
    111 changes: 111 additions & 0 deletions observable.swift
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,111 @@
    import Foundation

    class Disposable {
    let dispose: () -> Void
    init(dispose: @escaping () -> Void) { self.dispose = dispose }
    deinit {
    dispose()
    }
    }

    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 value

    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> {

    var value: T {
    didSet {
    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 Disposable { [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?) {
    for observer in observers.values {
    observer(value, oldValue)
    }
    }

    func notifyAllObservers() {
    notifyAllObservers(oldValue: nil)
    }
    }

    struct ObservableOptions: OptionSet {
    let rawValue: Int
    static let initial = ObservableOptions(rawValue: 1 << 0)
    }

    extension Observable: CustomStringConvertible {
    var description: String {
    return "\(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 }
    for observer in observers.values {
    observer(value, oldValue)
    }
    }

    }

    /*! Helper extension for observers of `Observable`

    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: class {
    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 = DisposeBag()
    }
    }