Skip to content

Instantly share code, notes, and snippets.

@IanKeen
Last active February 24, 2017 01:15
Show Gist options
  • Save IanKeen/f9dc19cf596c5766ee5d3f87472c4043 to your computer and use it in GitHub Desktop.
Save IanKeen/f9dc19cf596c5766ee5d3f87472c4043 to your computer and use it in GitHub Desktop.
Swifty KVO - Type-safe access, no 'stringly-typed' garbage here ;)
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() }
}
}
name: String foo
age: Int 42
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
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
}
@IanKeen
Copy link
Author

IanKeen commented Feb 24, 2017

Updated for Swift 3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment