Skip to content

Instantly share code, notes, and snippets.

@thecodewarrior
Last active April 26, 2018 00:26
Show Gist options
  • Save thecodewarrior/86d825529dbb7fc22b659935af4c9874 to your computer and use it in GitHub Desktop.
Save thecodewarrior/86d825529dbb7fc22b659935af4c9874 to your computer and use it in GitHub Desktop.
import Foundation
protocol AssociatedValueType: class {}
extension AssociatedValueType {
var associatedDictionary: AssociatedDictionary {
return AssociatedDictionary.associatedDictionary(for: self)
}
}
class AssociatedDictionary {
fileprivate static var key = "GNT_AssociatedDictionary"
static func associatedDictionary(for object: Any) -> AssociatedDictionary {
if let dict = objc_getAssociatedObject( object, &AssociatedDictionary.key) as? AssociatedDictionary {
return dict
} else {
let dict = AssociatedDictionary()
objc_setAssociatedObject( object, &AssociatedDictionary.key, dict, .OBJC_ASSOCIATION_RETAIN)
return dict
}
}
fileprivate var map: [HashableType: [String: Any]] = [:]
subscript<T: Any>(key: String) -> T? {
get {
return self[key, T.self]
}
set(newValue) {
self[key, T.self] = newValue
}
}
subscript<T: Any>(key: String, type: T.Type) -> T? {
get {
return map[HashableType(T.self)]?[key] as? T
}
set(newValue) {
var typeMap = map[HashableType(T.self)] ?? [:]
typeMap[key] = newValue
map[HashableType(T.self)] = typeMap
}
}
@discardableResult
func remove<T: Any>(_ key: String, type: T.Type = T.self) -> T? {
return map[HashableType(T.self)]?.removeValue(forKey: key) as? T
}
func getOrSet<T: Any>(_ key: String, default generator: @autoclosure () -> T) -> T {
if let value: T = self[key] {
return value
} else {
let value = generator()
self[key] = value
return value
}
}
}
struct HashableType: Hashable {
static func == (lhs: HashableType, rhs: HashableType) -> Bool {
return lhs.base == rhs.base
}
let base: Any.Type
init(_ base: Any.Type) {
self.base = base
}
var hashValue: Int {
return ObjectIdentifier(base).hashValue
}
}
// ======================================= Defining what types are supported ======================================== //
// ====================================== (Because we can't extend AnyObject) ======================================= //
extension NSObject: AssociatedValueType {}
import Foundation
#if canImport(BrightFutures)
import BrightFutures
import Result
#endif
extension NSKeyValueObservation {
/// Stores this key in the passed object so its lifetime will be that of the passed object
@discardableResult
func attach(to attachment: AnyObject?) -> Self {
let list: Cell<[NSKeyValueObservation]> = AssociatedDictionary.associatedDictionary(for: attachment)
.getOrSet("AttachedKeyValueObservationList", default: Cell([NSKeyValueObservation]()))
list.value.append(self)
return self
}
@discardableResult
func detach(from attachment: AnyObject?) -> Self {
let list: Cell<[NSKeyValueObservation]> = AssociatedDictionary.associatedDictionary(for: attachment)
.getOrSet("AttachedKeyValueObservationList", default: Cell([NSKeyValueObservation]()))
list.value.remove(where: { $0 === self })
return self
}
}
protocol KeyValueObservable {}
extension KeyValueObservable where Self: NSObject {
/// Creates a Key-Value Observer for the passed keypath and stores it in `attachment` so it has the same lifetime.
@discardableResult
func observe<Value>(
_ keyPath: KeyPath<Self, Value>,
options: NSKeyValueObservingOptions = [],
attachment: AnyObject?,
changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void)
-> NSKeyValueObservation {
// Note: In case of fatal error, make sure observed property is marked `@objc dynamic`
return self.observe(keyPath, options: options, changeHandler: changeHandler).attach(to: attachment)
}
/// Creates a Key-Value Observer for the passed keypath and stores it in `attachment` so it has the same lifetime.
/// The observer will invalidate itself after the first call
@discardableResult
func observeOnce<Value>(
_ keyPath: KeyPath<Self, Value>,
options: NSKeyValueObservingOptions = [],
attachment: AnyObject?,
changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void)
-> NSKeyValueObservation {
var observation: NSKeyValueObservation!
// Note: In case of fatal error, make sure observed property is marked `@objc dynamic`
observation = self.observe(keyPath, options: options,
changeHandler: { (object: Self, change: NSKeyValueObservedChange<Value>) in
changeHandler(object, change)
observation.invalidate()
})
if let attachment = attachment {
observation.attach(to: attachment)
}
return observation
}
/// Creates a Key-Value Observer for the passed keypath and stores it in `attachment` so it has the same lifetime.
/// The observer will invalidate itself after the first call whose kind matches the passed NSKeyValueChange
@discardableResult
func observe<Value>(
_ keyPath: KeyPath<Self, Value>,
options: NSKeyValueObservingOptions = [],
attachment: AnyObject?,
until targetChange: NSKeyValueChange,
changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void)
-> NSKeyValueObservation {
var observation: NSKeyValueObservation!
// Note: In case of fatal error, make sure observed property is marked `@objc dynamic`
observation = self.observe(keyPath, options: options,
changeHandler: { (object: Self, change: NSKeyValueObservedChange<Value>) in
changeHandler(object, change)
if change.kind == targetChange {
observation.invalidate()
}
})
if let attachment = attachment {
observation.attach(to: attachment)
}
return observation
}
/// Gets the current value of the keypath, and if it is nonnil, passes `self` and the value to `valueHandler`.
/// If the current value is nil the valueHandler will be called when it is set to a nonnil value.
/// In the latter case the observer is stored in `attachment` so it has the same lifetime.
/// - returns: The NSKeyValueObservation if the current value is nil
@discardableResult
func getOrObserveSet<Value>(
_ keyPath: KeyPath<Self, Value?>,
attachment: AnyObject??,
valueHandler: @escaping (Self, Value) -> Void)
-> NSKeyValueObservation? {
let existingValue = self[keyPath: keyPath]
if existingValue is Value {
valueHandler(self, existingValue as! Value) // swiftlint:disable:this force_cast
return nil
} else {
var observation: NSKeyValueObservation!
// Note: In case of fatal error, make sure observed property is marked `@objc dynamic`
observation = self.observe(keyPath, options: [.new],
changeHandler: { (object: Self, change: NSKeyValueObservedChange<Value?>) in
if change.kind == .setting {
let newValue = change.newValue
if newValue is Value {
valueHandler(object, newValue as! Value) // swiftlint:disable:this force_cast
observation.detach(from: object)
observation.invalidate()
}
}
})
if let attachment = attachment {
observation.attach(to: attachment)
}
return observation
}
}
/// Gets the current value of the keypath and passes `self` and the value to `valueHandler`.
/// From that point onward valueHandler will be called whenever the value is set.
/// The observer used to process updates is stored in `attachment` so it has the same lifetime.
@discardableResult
func observeCurrentAndChanges<Value>(
_ keyPath: KeyPath<Self, Value>,
attachment: AnyObject?,
valueHandler: @escaping (Self, Value) -> Void)
-> NSKeyValueObservation {
let existingValue = self[keyPath: keyPath]
if existingValue is Value {
valueHandler(self, existingValue as! Value) // swiftlint:disable:this force_cast
}
var observation: NSKeyValueObservation
// Note: In case of fatal error, make sure observed property is marked `@objc dynamic`
observation = self.observe(keyPath, options: [.new],
changeHandler: { (object: Self, change: NSKeyValueObservedChange<Value>) in
if change.kind == .setting {
if change.newValue is Value {
valueHandler(object, change.newValue as! Value) // swiftlint:disable:this force_cast
}
}
})
if let attachment = attachment {
observation.attach(to: attachment)
}
return observation
}
/// Every time the value at the keypath is set this passes `self` and the new value to `valueHandler`.
/// The observer used to process updates is stored in `attachment` so it has the same lifetime.
@discardableResult
func observeChanges<Value>(
_ keyPath: KeyPath<Self, Value>,
attachment: AnyObject?,
valueHandler: @escaping (Self, Value) -> Void)
-> NSKeyValueObservation {
var observation: NSKeyValueObservation
// Note: In case of fatal error, make sure observed property is marked `@objc dynamic`
observation = self.observe(keyPath, options: [.new],
changeHandler: { (object: Self, change: NSKeyValueObservedChange<Value>) in
if change.kind == .setting {
if change.newValue is Value {
valueHandler(object, change.newValue as! Value) // swiftlint:disable:this force_cast
}
}
})
if let attachment = attachment {
observation.attach(to: attachment)
}
return observation
}
#if canImport(BrightFutures)
/// Creates a future that will complete when the value of the passed attribute is non-null
@discardableResult
func future<Value>(for keyPath: KeyPath<Self, Value?>) -> Future<Value, NoError> {
var futures = AssociatedDictionary.associatedDictionary(for: self)
.getOrSet("KVOFutures", default: KVOFutureStorage())
if let existingFuture = futures.map[keyPath] {
return existingFuture as! Future<Value, NoError> // swiftlint:disable:this force_cast
}
if let existingValue = self[keyPath: keyPath] {
let newFuture = Future<Value, NoError>(value: existingValue)
futures.map[keyPath] = newFuture
return newFuture
} else {
let newPromise = Promise<Value, NoError>()
var observation: NSKeyValueObservation!
// Note: In case of fatal error, make sure observed property is marked `@objc dynamic`
observation = self.observe(keyPath, options: [.new],
changeHandler: { (object: Self, change: NSKeyValueObservedChange<Value?>) in
if let newValue = change.newValue!, change.kind == .setting {
newPromise.success(newValue)
observation.detach(from: object)
observation.invalidate()
}
})
observation.attach(to: self)
return newPromise.future
}
}
#endif
}
class KVOFutureStorage {
// can't make the value Future<Any, NoError> because Futures are generic...
// but I _can_ make the value literally anything. Makes sense... ಠ_ಠ
var map = [AnyKeyPath: Any]()
}
extension NSObject: KeyValueObservable {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment