KVC 与 KVO 拾遗补缺
KVC 和 KVO 是 Cocoa 框架提供的一个非常强的特性,使用好它们能大大提高我们的开发效率,今天咱们就来探讨一下关于 KVO 需要注意的事情。
前一篇文章中,我们和大家一起分析了 KVC 的特性机制以及要注意的问题。建议大家先看一下这篇文章:漫谈 KVC 与 KVO 对 KVC 有一个了解。我们这次和大家分析 KVO 的相关内容。
如何使用
KVO 就是一种监听属性变化并作出响应的机制。这个特性在我们日常开发中应用很广泛,比如一个 UILabel 用于显示某个模型的属性值,当这个属性值改变的时候,自动更新 UILabel 的显示。
KVO 的基本接口并不复杂,我们来看一个例子, 首先我们定义一个实体类:
class Person: NSObject {
dynamic var firstName: String
dynamic var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
这个类有两个属性 firstName 和 lastName,它继承自 NSObject ,所以它对 KVC 协议提供了默认实现。注意一下这两个属性的 dynamic
标识。这个代表它支持 Objective-C Runtime 的动态分发机制,我们这里可以理解为 KVO 需要需要使用 Objective-C Runtime 的这个机制来实现属性更改的监听。 Swift 中的属性处于性能等方面的考虑默认是关闭动态分发的,所以我们这里面要显示的将属性用 dynamic
关键字标识出来。
然后我们定义一个 ViewController, 将 KVO 所有的处理都写在这里:
class Controller: UIViewController {
var labelFirstName: UILabel?
var labelLastName: UILabel?
var person:Person?
override func viewDidLoad() {
self.labelFirstName = UILabel(frame: CGRectMake(0, 0, 0, 0))
self.labelLastName = UILabel(frame: CGRectMake(0, 0, 0, 0))
self.view.addSubview(self.labelFirstName!)
self.view.addSubview(self.labelLastName!)
self.person = Person(firstName: "peter", lastName: "cook")
self.person?.addObserver(self, forKeyPath: "firstName", options: NSKeyValueObservingOptions.New, context: nil)
self.person?.addObserver(self, forKeyPath: "lastName", options: NSKeyValueObservingOptions.New, context: nil)
}
}
这个 ViewController 在它的 viewDidLoad 方法中初始化了两个 UILabel 和 Person 类,然后通过两个方法调用将 KVO 属性监听注册给这个 ViewController:
self.person?.addObserver(self, forKeyPath: "firstName", options: NSKeyValueObservingOptions.New, context: nil)
self.person?.addObserver(self, forKeyPath: "lastName", options: NSKeyValueObservingOptions.New, context: nil)
addObserver
方法用于将实体类的属性注册给 KVO 监听对象。我们将 self.person 的两个属性 firstName 和 lastName(addObserver 方法的 forKeyPath 参数) 注册给了 self(addObserver 方法的第一个参数) ,然后我们指定了监听选项 NSKeyValueObservingOptions.New
, 这个选项代表我们监听 KVO 每次属性改变后的新值。
我们注册好了监听方法后,还需要在属性值改变的时候处理这个消息,这就需要我们的 ViewController 再实现一个 observeValueForKeyPath
方法:
class Controller: UIViewController {
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "firstName" {
if let firstName = change?[NSKeyValueChangeNewKey] as? String {
self.labelFirstName?.text = firstName
}
} else if keyPath == "lastName" {
if let lastName = change?[NSKeyValueChangeNewKey] as? String {
self.labelLastName?.text = lastName
}
}
}
}
每当检测到属性的改变,我们会判断对应的 keyPath,然后更新相应的 Label 的文本。
addObserver
addObserver 方法是 KVO 的一个关键方法,用来添加监听者与被监听者的关系。它的逻辑关系需要我们注意一下,我们是在被监听的对象上面调用这个方法,然后将监听者对象传递给这个方法,就像我们前面调用的一样:
self.person?.addObserver(self, forKeyPath: "firstName", options: NSKeyValueObservingOptions.New, context: nil)
第一个参数 self 就是监听者对象。然后还需要一个 keyPath 参数,它用于描述我们要监听哪个属性,比如我们这里监听 firstName 属性的改变。
紧接着的 options 参数指定了监听选项,我们可以指定如下参数:
- NSKeyValueObservingOptions.New 每次属性改变后的新值
- NSKeyValueObservingOptions.Old 每次属性改变之前的旧值
最后,在控制器被销毁的时候,我们要将 KVO 通知的删除掉(如果没有正确的清除掉 KVO 通知,程序可能会在某些时候以外的崩溃):
deinit {
super.viewDidLoad()
self.person?.removeObserver(self, forKeyPath: "firstName")
self.person?.removeObserver(self, forKeyPath: "lastName")
}
这样, KVO 基本流程就完成了。
属性依赖
我们还会遇到这样的情况,比如有一个属性,它的值是依赖于另外的属性,还是以 Person 类为例,添加一个 fullName 属性:
var fullName: String {
get {
return "\(lastName) \(firstName)"
}
}
这个属性值是通过 lastName 和 firstName 这两个属性的值生成的。所以当这两个属性改变的时候,也相当于 fullName 的值也改变了。
对于这样的属性关系,我们可以通过实现 keyPathsForValuesAffectingValueForKey
方法在实体类中声明属性依赖,以 fullName 属性为例:
class Person: NSObject {
override class func keyPathsForValuesAffectingValueForKey(key: String) -> Set<String> {
if key == "fullName" {
return Set<String>(arrayLiteral: "firstName","lastName")
} else {
return super.keyPathsForValuesAffectingValueForKey(key)
}
}
}
这样,我们声明了 fullName 属性依赖于两个其他属性 lastName,firstName。在 lastName 和 firstName 的属性值改变后,也会触发 fullName 属性改变的通知。
注意:覆盖 keyPathsForValuesAffectingValueForKey 方法的时候有一点需要注意,这个方法在 Objective-C 中的签名是这样:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
。如果照着这个逻辑,我们很可能在 Swift 中将这个方法的声明写成这样:override class func keyPathsForValuesAffectingValueForKey(key: NSString) -> NSSet。 注意这里面参数和返回值的类型,如果这样写,编译器就会报错。因为 Swift 中将 Objective-C 的一些基础类型都已经转变成了 Swift 的原生类型,所以我们这个方法签名要写成这样才可以通过编译:override class func keyPathsForValuesAffectingValueForKey(key: String) -> Set<String>
自动通知与手动通知
KVO 在默认情况下,只要为某个属性添加了监听对象,在这个属性值改变的时候,就会自动的通知监听者。也有一些情况下,可能我们想手动的处理这些通知的发送, KVO 也是允许我们这样做的。
可以通过覆盖 automaticallyNotifiesObserversForKey 方法来告诉 KVO,那些属性是我们想手动处理的。 比如我们的 Person 类中,相对 firstName 进行处理,就在 automaticallyNotifiesObserversForKey 方法中对 firstName 这个 key 返回 false:
class Person: NSObject {
//...
override class func automaticallyNotifiesObserversForKey(key:String) -> Bool {
if key == "firstName" {
return false
} else {
return true
}
}
//...
}
这样,我们在修改了 fistName 属性值后,就不会触发 KVO 的默认通知行为,是我们自己来控制通知的发送,我们需要修改 firstName 属性的实现来进行手工的通知发送:
class Person: NSObject {
var _firstName:String
dynamic var firstName: String {
set {
self.willChangeValueForKey("firstName")
_firstName = newValue
self.didChangeValueForKey("firstName")
}
get{
return _firstName
}
}
}
手动通知能让我们对 KVO 通知进行更细节的控制。但并不常用,大多数情况下使用 KVO 的自动通知机制就足够了。
NSKeyValueObservingOptions.Initial
我们在进行 UI 相关的 KVO 操作时候,通常会遇到这样的需求,在添加通知后,立即发送一个改变通知告诉 UI 去更新界面,这样让我们的界面有一个初始状态。我们可以指定 NSKeyValueObservingOptions.Initial 选项,这样在我们添加完 KVO 监听后,属性改变的通知就会立即被执行一次:
self.person?.addObserver(self, forKeyPath: "firstName", options: [NSKeyValueObservingOptions.New,NSKeyValueObservingOptions.Initial], context: nil)
NSKeyValueObservingOptions.Prior
NSKeyValueObservingOptions.Prior 这个选项可以让我们在被监听的属性改变的时候得到两个通知,一个是在属性值改变之前,一个是属性值改变之后。然后就可以在 observeValueForKeyPath 中的 change 字典中以 NSKeyValueChangeNotificationIsPriorKey 键来表示当前通知是不是在属性被修改之前发送的:
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let _ = change?[NSKeyValueChangeNotificationIsPriorKey] {
print("old")
}else {
print("new")
}
}
当然,如果你只是想得到修改前和修改后的值,那么也可以用 change 字段中的 NSKeyValueChangeOldKey 和 NSKeyValueChangeNewKey 来得到相应的值:
if let newFirstName = change?[NSKeyValueChangeNewKey] as? String {
}
if let oldFirstName = change?[NSKeyValueChangeOldKey] as? String {
}
总结
KVO 是 Cocoa 提供的一个很强大的特性,但同时它也有很多坑需要我们注意,比如添加完监听后,要在不需要的时候删除掉监听,否则就会造成意外崩溃。对于有依赖关系的属性需要通过 keyPathsForValuesAffectingValueForKey
方法将依赖关系声明到实体类中。以及各个监听选项的作用。还有 Swift 中使用 KVO 特别要注意的那些地方。
熟练使用 KVO 无疑会对我们的开发有很大的帮助,这篇文章我们将 KVO 大部分特性以及需要注意的地方总结了一下,当然算不上特别全面,但希望能通过它帮助大家拓展思路,有所帮助。
更多精彩内容可关注微信公众号:
swift-cafe