Last active
August 25, 2017 16:29
-
-
Save crayment/734d4c93533715750e55be2e86d509aa to your computer and use it in GitHub Desktop.
Observable
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
let foo: Observable<String> = Observable("1") | |
print(foo.get()) | |
foo.addObserver(self) { (newValue) in | |
print(newValue) | |
} | |
foo.set("12") | |
foo.set("123") | |
foo.set("1234") | |
/* Prints: | |
1 | |
12 | |
123 | |
1234 | |
*/ |
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 Foundation | |
/** | |
Observable wraps an internal value providing a simple API for consumers to be notified of changes to the value. | |
You add an observation by specifying an `observer` which should be thought of as a lifetime object. When the `observer` | |
is deallocated the observation is removed and the closure will not be called again. It is possible to unsubscribe | |
earlier by passing the `observer` to `removeObserver(_:)`. Note though that this removes all observations added using | |
this `observer`. | |
Example: | |
`` | |
let bar: Observable<String> = Observable("") | |
bar.addObserver(self) { print($0) } | |
bar.set("new value") // prints "new value" | |
bar.addObserver(self) { print($0) } | |
bar.removeObserver(self) // Removes both observers. Automatically removed if `self` was deallocated. | |
``` | |
*/ | |
open class Observable<Type> { | |
private let set = ObserverSet<Type>() | |
private var value: Type { | |
didSet { | |
set.notify(value) | |
} | |
} | |
init(_ value: Type) { | |
self.value = value | |
} | |
/** | |
Add a closure that will be called until `observer` is deallocated or `removeObserver(_:)` | |
is called with the `observer`. | |
- Parameter observer: The observing object that when released will end the observation. | |
This object can also be passed to `removeObserver(_:)` to remove all observers that were | |
added with this object before it is deallocated. | |
- Parameter closure: The closure to be called when the value changes. | |
Example: | |
``` | |
let bar: Observable<String> = Observable("") | |
bar.addObserver(self) { print($0) } | |
bar.set("new value") // Prints "new value" | |
// later... | |
bar.removeObserver(self) | |
``` | |
*/ | |
func addObserver<T: AnyObject>(_ observer: T, _ closure: @escaping (Type) -> Void) { | |
set.add(observer, { _ in closure }) | |
} | |
/** | |
Remove all observations added with the observer. | |
- Parameter observer: An observer object that was used in `addObserver(_:_:)` | |
Example: | |
``` | |
let bar: Observable<String> = Observable("") | |
// First Pattern | |
bar.addObserver(self) { print($0) } | |
bar.addObserver(self) { print($0) } | |
bar.removeObserver(self) // Removes both observers. Automatically removed if `self` was deallocated. | |
``` | |
*/ | |
func removeObserver(_ observer: AnyObject) { | |
set.removeAllWithObject(observer) | |
} | |
/** | |
Set a new value | |
- Parameter newValue: the new value to set | |
*/ | |
func set(_ newValue: Type) { | |
value = newValue | |
} | |
/** | |
Get the value | |
- Returns: the value | |
*/ | |
func get() -> Type { | |
return value | |
} | |
} | |
private class ObserverSetEntry<Parameters> { | |
fileprivate private(set) weak var object: AnyObject? | |
fileprivate let closure: (AnyObject) -> (Parameters) -> Void | |
fileprivate init(object: AnyObject, closure: @escaping (AnyObject) -> (Parameters) -> Void) { | |
self.object = object | |
self.closure = closure | |
} | |
} | |
private class ObserverSet<Parameters>: CustomStringConvertible { | |
// Locking support | |
private var queue = DispatchQueue(label: "com.mikeash.ObserverSet", attributes: []) | |
private let notifyQueue = DispatchQueue.main | |
private func synchronized(_ closure: () -> Void) { | |
queue.sync(execute: closure) | |
} | |
// Main implementation | |
private var entries: [ObserverSetEntry<Parameters>] = [] | |
public init() {} | |
@discardableResult | |
fileprivate func add<T: AnyObject>(_ object: T, _ closure: @escaping (T) -> (Parameters) -> Void) -> ObserverSetEntry<Parameters> { | |
// swiftlint:disable force_cast | |
let entry = ObserverSetEntry<Parameters>(object: object, closure: { closure($0 as! T) }) | |
// swiftlint:enable force_cast | |
synchronized { | |
self.entries = self.entries.filter { $0.object != nil } | |
self.entries.append(entry) | |
} | |
return entry | |
} | |
fileprivate func add(_ closure: @escaping (Parameters) -> Void) -> ObserverSetEntry<Parameters> { | |
return self.add(self, { _ in closure }) | |
} | |
fileprivate func remove(_ entry: ObserverSetEntry<Parameters>) { | |
synchronized { | |
self.entries = self.entries.filter { $0 !== entry } | |
} | |
} | |
fileprivate func removeAllWithObject(_ object: AnyObject) { | |
synchronized { | |
self.entries = self.entries.filter { $0.object !== object } | |
} | |
} | |
fileprivate func notify(_ parameters: Parameters) { | |
var toCall: [(Parameters) -> Void] = [] | |
synchronized { | |
for entry in self.entries { | |
if let object: AnyObject = entry.object { | |
toCall.append(entry.closure(object)) | |
} | |
} | |
self.entries = self.entries.filter { $0.object != nil } | |
} | |
notifyQueue.async { | |
for f in toCall { | |
f(parameters) | |
} | |
} | |
} | |
// MARK: CustomStringConvertible | |
fileprivate var description: String { | |
var entries: [ObserverSetEntry<Parameters>] = [] | |
synchronized { | |
entries = self.entries | |
} | |
let strings = entries.map { entry in | |
(entry.object === self | |
? "\(entry.closure)" | |
: "\(String(describing: entry.object)) \(entry.closure)") | |
} | |
let joined = strings.joined(separator: ", ") | |
return "\(Mirror(reflecting: self).description): (\(joined))" | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment