Skip to content

Instantly share code, notes, and snippets.

@crayment
Last active August 25, 2017 16:29
Show Gist options
  • Save crayment/734d4c93533715750e55be2e86d509aa to your computer and use it in GitHub Desktop.
Save crayment/734d4c93533715750e55be2e86d509aa to your computer and use it in GitHub Desktop.
Observable
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
*/
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