Last active
April 26, 2018 00:26
-
-
Save thecodewarrior/86d825529dbb7fc22b659935af4c9874 to your computer and use it in GitHub Desktop.
A hopefully more portable version of https://gist.github.com/thecodewarrior/c312e711b3d1c38385dcc52fa325d0b4
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 | |
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 {} |
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 | |
#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