Skip to content

Instantly share code, notes, and snippets.

@lorentey
Last active November 3, 2016 13:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lorentey/30c18028316afa48c1b3820a6109a414 to your computer and use it in GitHub Desktop.
Save lorentey/30c18028316afa48c1b3820a6109a414 to your computer and use it in GitHub Desktop.
import Foundation
@objc class Person: NSObject {
var _firstName: String = "John"
var _lastName: String = "Smith"
dynamic var firstName: String { get { return _firstName } set { _firstName = newValue } }
dynamic var lastName: String { get { return _lastName } set { _lastName = newValue } }
dynamic var name: String { return "\(firstName) \(lastName)" }
class func keyPathsForValuesAffectingName() -> NSSet { return ["firstName", "lastName"] }
}
class Observer: NSObject {
let object: AnyObject
let keyPath: String
var context: UInt8 = 0
init(object: AnyObject, keyPath: String) {
self.object = object
self.keyPath = keyPath
super.init()
object.addObserver(self, forKeyPath: keyPath, options: [.prior, .old, .new], context: &context)
}
deinit {
object.removeObserver(self, forKeyPath: keyPath, context: &context)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?,
change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &self.context {
guard let change = change else { return }
let old = change[.oldKey] == nil || change[.oldKey] as? NSNull != nil ? "nil" : "\(change[.oldKey]!)"
let new = change[.newKey] == nil || change[.newKey] as? NSNull != nil ? "nil" : "\(change[.newKey]!)"
if change[.notificationIsPriorKey] as? Bool == true {
print("willChange(old: \(old))")
}
else {
print("didChange(old: \(old), new: \(new))")
}
}
else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
}
}
}
let person = Person()
let name = Observer(object: person, keyPath: "name")
person.willChangeValue(forKey: "firstName")
person.willChangeValue(forKey: "lastName")
person._firstName = "Mary"
person._lastName = "Taylor"
person.didChangeValue(forKey: "lastName")
person.didChangeValue(forKey: "firstName")
withExtendedLifetime(person) {}
@Gernot
Copy link

Gernot commented Nov 3, 2016

Here's how I would do it. Basically it's more code and it sucks more, but that's how KVO is done unfortunately. It's a but like in the old Objective-C days when you had to write the stored properties yourself and did't have synthesised accessors. NEVER use the stored _properties from outside of the class, always use accessors or methods. The automatic "keyPathsForValuesAffecting" would trigger twice but sequentially if you don't use the new function that sets both at the same time, but that's ok and expected.

import Foundation

@objc class Person: NSObject {
    var _firstName: String = "John"
    var _lastName: String = "Smith"
    
    dynamic var firstName: String {
        get { return _firstName }
        set {
            willChangeValue(forKey: "name")
            willChangeValue(forKey: "firstName")
            _firstName = newValue
            didChangeValue(forKey: "firstName")
            didChangeValue(forKey: "name")
        }
    }
    
    dynamic var lastName: String {
        get { return _lastName }
        set {
            willChangeValue(forKey: "name")
            willChangeValue(forKey: "lastName")
            _lastName = newValue
            didChangeValue(forKey: "lastName")
            didChangeValue(forKey: "name")
        }
    }
    
    dynamic var name: String { return "\(firstName) \(lastName)" }

    func set(firstName: String, lastName: String) {
        
        willChangeValue(forKey: "name")
        willChangeValue(forKey: "firstName")
        willChangeValue(forKey: "lastName")
        
        _firstName = firstName
        _lastName = lastName
        
        didChangeValue(forKey: "lastName")
        didChangeValue(forKey: "firstName")
        didChangeValue(forKey: "name")
        
    }
    
}

class Observer: NSObject {
    let object: AnyObject
    let keyPath: String
    var context: UInt8 = 0
    
    init(object: AnyObject, keyPath: String) {
        self.object = object
        self.keyPath = keyPath
        super.init()
        object.addObserver(self, forKeyPath: keyPath, options: [.prior, .old, .new], context: &context)
    }
    
    deinit {
        object.removeObserver(self, forKeyPath: keyPath, context: &context)
    }
    
    override func observeValue(forKeyPath keyPath: String?, of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
        if context == &self.context {
            guard let change = change else { return }
            let old = change[.oldKey] == nil || change[.oldKey] as? NSNull != nil ? "nil" : "\(change[.oldKey]!)"
            let new = change[.newKey] == nil || change[.newKey] as? NSNull != nil ? "nil" : "\(change[.newKey]!)"
            if change[.notificationIsPriorKey] as? Bool == true {
                print("willChange(old: \(old))")
            }
            else {
                print("didChange(old: \(old), new: \(new))")
            }
        }
        else {
            super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        }
    }
}

let person = Person()
let name = Observer(object: person, keyPath: "name")

person.set(firstName: "Mary", lastName: "Taylor")

withExtendedLifetime(person) {}

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