Last active
February 24, 2017 01:15
-
-
Save IanKeen/f9dc19cf596c5766ee5d3f87472c4043 to your computer and use it in GitHub Desktop.
Swifty KVO - Type-safe access, no 'stringly-typed' garbage here ;)
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
public protocol KeyPathCollection: RawRepresentable, Hashable { | |
static var allKeyPaths: [Self] { get } | |
} | |
public extension KeyPathCollection where RawValue: Hashable { | |
var hashValue: Int { return self.rawValue.hashValue } | |
} | |
class KeyPathBox { | |
weak var object: AnyObject? | |
var closure: () -> Void | |
init(object: AnyObject, closure: @escaping () -> Void) { | |
self.object = object | |
self.closure = closure | |
} | |
} | |
public class KeyPathObserver<KeyPath: KeyPathCollection>: NSObject where KeyPath.RawValue == String { | |
//MARK: - Private Properties | |
private typealias KeyPathClosure = () -> Void | |
private let target: AnyObject | |
private let allKeyPaths: [KeyPath] | |
private var observers = [KeyPath: [KeyPathBox]]() | |
//MARK: - Lifecycle | |
public required init(target: AnyObject) { | |
self.allKeyPaths = KeyPath.allKeyPaths | |
self.target = target | |
super.init() | |
self.observeKeyPaths() | |
} | |
deinit { self.unobserveKeyPaths() } | |
//MARK: - KVO | |
private var context: UInt8 = 0 | |
private func observeKeyPaths() { | |
for k in KeyPath.allKeyPaths { | |
self.target.addObserver(self, forKeyPath: k.rawValue, options: .new, context: &context) | |
} | |
} | |
private func unobserveKeyPaths() { | |
for k in KeyPath.allKeyPaths { | |
self.target.removeObserver(self, forKeyPath: k.rawValue) | |
} | |
} | |
//MARK: - Dispatch | |
public func onChange<T>(_ object: AnyObject, _ keyPath: KeyPath, closure: @escaping (KeyPath, T) -> Void) { | |
var items = self.observers[keyPath] ?? [] | |
let result = { | |
guard let value = self.target.value(forKey: keyPath.rawValue) else { return } | |
if let value = value as? T { | |
closure(keyPath, value) | |
} else { | |
let mirror = Mirror(reflecting: value) | |
let expectedType: Any.Type | |
if let firstChild = mirror.children.first?.value { | |
expectedType = type(of: firstChild) | |
} else { | |
expectedType = type(of: value) | |
} | |
fatalError("Type Mismatch: Expected \(T.self) got \(expectedType)") | |
} | |
} | |
items.append(KeyPathBox(object: object, closure: result)) | |
self.observers[keyPath] = items | |
} | |
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { | |
guard | |
context == &self.context, | |
let keyPath = keyPath, | |
let keypath = KeyPath(rawValue: keyPath), | |
let observers = self.observers[keypath] | |
else { return } | |
observers | |
.filter { $0.object != nil } | |
.forEach { $0.closure() } | |
} | |
} |
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
name: String foo | |
age: Int 42 |
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 = Foo() | |
foo.observer.onChange(self, .name) { (kp: Foo.KeyPath, name: String) in | |
print("\(kp): \(name.dynamicType.self) \(name)") | |
} | |
foo.observer.onChange(self, .age) { (kp: Foo.KeyPath, age: Int) in | |
print("\(kp): \(age.dynamicType.self) \(age)") | |
} | |
foo.name = "foo" | |
foo.age = 42 |
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
class Foo: NSObject { | |
//MARK: - KeyPath | |
lazy var observer: KeyPathObserver<KeyPath> = KeyPathObserver<KeyPath>(target: self) | |
enum KeyPath: String, Hashable, KeyPathCollection { | |
case name | |
case age | |
static let allKeyPaths: [KeyPath] = [.name, .age] | |
} | |
//MARK: - Properties | |
dynamic var name: String = "" | |
dynamic var age: Int = 0 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Updated for Swift 3