KVC 与 KVO 无疑是 Cocoa 提供给我们的一个非常强大的特性,使用熟练可以让我们的代码变得非常简洁并且易读。但 KVC 与 KVO 提供的 API 又是比较复杂的,绝对超出我们不经深究之前所理解到的复杂度,这次大家就来跟我一起深入认识这两个特性吧。
基础使用
首先,咱们要说的是 KVC (Key-Value Coding), 它是一种用间接方式访问类的属性的机制。在 Swift 中为一个类实现 KVC 的话,需要让它继承自 NSObject:
class Person: NSObject {
var firstName: String
var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
}
这样,我们就可以使用 KVC 的方式访问 Person 类的属性了:
let peter = Person(firstName: "Cook", lastName: "Peter")
print(peter.lastName)
print(peter.valueForKey("lastName")!)
注意我们的两个 print 语句,第一个是使用直接引用属性的方式,第二个就是使用 KVC 机制访问的方式。 valueForKey 是 KVC 协议中定义的方法,它接受一个参数,我们把它叫做 key,这个 key 表示要访问的属性名称,KVC 就会根据我们传入的 key 帮助我们找到对应的属性。
不同之处
在 Swift 中处理 KVC和 Objective-C 中还是有些细微的差别。比如,Objective-C 中所有的类都继承自 NSObject,而 Swift 中却不是,所以我们在 Swift 中需要显式的声明继承自 NSObject。
可为什么要继承自 NSObject 呢?我们在苹果官方的 KVC 文档中找到了答案。其实 KVC 机制是由一个协议 NSKeyValueCoding
定义的。NSObject 帮我们实现了这个协议,所以 KVC 核心的逻辑都在 NSObject 中,我们继承 NSObject 才能让我们的类获得 KVC 的能力。(理论上说,如果你遵循 NSKeyValueCoding
协议的接口,其实也可以自己实现 KVC 的细节,完全行得通。但在实践上,这么做就不太值得了,太费时间了~)。
另外,因为 Swift 中的 Optional 机制,所以 valueForKey 方法返回的是一个 Optional 值,我们还需要对返回值做一次解包处理,才能得到实际的属性值。
那么书归正传,KVC 最主要的好处是什么呢,简单来说就是我们可以不用过多的依赖编译时的限制,而是为我们提供了更多的运行时的能力。
valueForUndefinedKey
还是继续咱们上面的例子,假如我们又写了这样一个语句会怎么样呢:
peter.valueForKey("noExist")
因为我们定义的 Person 类中是没有 noExist 这个属性的,所以 KVC 也无法找到这个属性值,这时候 KVC 协议其实会调用 valueForUndefinedKey 方法,NSObject 对这个方法的默认实现是抛出一个 NSUndefinedKeyException 异常。所以如果我们没有自己重写 valueForUndefinedKey 方法的话,这时应用就会因为异常崩溃。
我们也可以在 Person 类中实现我们自己的 valueForUndefinedKey 方法:
class PersonHandleUndefinedKey: NSObject {
var firstName: String
var lastName: String
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
}
override func valueForUndefinedKey(key: String) -> AnyObject? {
return ""
}
}
let peter2 = PersonHandleUndefinedKey(firstName: "Cook", lastName: "Peter")
print(peter2.valueForKey("noExist"))
这次定义了 valueForUndefinedKey 对于未定义的 key 返回一个空字符串,这样我们的 KVC 调用就能以更加优雅的方式处理这个异常行为了。
valueForKeyPath
KVC 除了可以用单个的 key 来访问单个属性,还提供了一个叫做 keyPath 的东西。所谓 keyPath,就比如你的属性本身也有自己的属性,那么想引用这个属性,就需要用到 keyPath。咱们用一个示例来说明:
class Address: NSObject {
var firstLine: String
var secondLine: String
init(firstLine: String, secondLine: String) {
self.firstLine = firstLine
self.secondLine = secondLine
}
}
class PersonHandleKeyPath: NSObject {
var firstName: String
var lastName: String
var address: Address
init(firstName: String, lastName: String, address: Address) {
self.firstName = firstName
self.lastName = lastName
self.address = address
}
}
var peter3 = PersonHandleKeyPath(firstName: "Cook", lastName: "Peter", address: Address(firstLine: "Beijing", secondLine: "Haidian"))
print(peter3.valueForKeyPath("address.firstLine")!)
PersonHandleKeyPath 类定义了一个属性 address
, 这个 address 本身又是一个类,它也有两个属性 firstLine
和 lastLine
, 那么我们如果想引用 address 的 firstLine 属性,就可以使用 KVC 的 keyPath 机制:
print(peter3.valueForKeyPath("address.firstLine")!)
通过 keyPath,我们可以使用 KVC 将属性引用范围扩大很多。这个规则对 Cocoa 系统类也适用,比如:
let view = UIView()
print(view.valueForKeyPath("superview.superview"))
我们可以通过 KVC 的这个机制遍历 UIView 层级。
同样的,如果 keyPath 中引用的任何一级属性不存在或者不符合 KVC 规范, valueForUndefinedKey 方法就会被调用。
SetValueForKey
KVC 定义了使用 valueForKey
方法获取属性的值,同样也提供了设置属性值的方法,就是 setValue:forKey
", 还是接着上面的例子:
peter3.setValue("swift", forKey: "firstName")
print(peter3.valueForKey("firstName")!)
setValue:forKey 方法接受两个参数,第一个参数是我们要设置的属性的值,第二个参数是属性的 key。这个接口很简单明了,就不多赘述了。
和 valueForKey 一样,如果我们给 setValue 传递一个不存在的 key 值,KVC 就会去调用 setValue: forUndefinedKey
方法,NSObject 对这个方法的默认实现依然是抛出一个 NSUndefinedKeyException 异常。
关于标量值
所谓标量值(Scalar Type),指的是简单类型的属性,比如 int,float 这些非对象的属性。关于标量值的在 KVC 中的处理有有些地方需要我们注意,我们把 Person 类再重写一下:
class PersonForScalar : NSObject {
var firstName: String
var lastName: String
var age: Int
init(firstName: String, lastName: String, age: Int) {
self.firstName = firstName
self.lastName = lastName
self.age = age
}
}
那么现在可以使用 KVC 来操作它的各个属性:
var person4 = PersonForScalar(firstName: "peter", lastName: "cook", age: 32)
person4.setValue(55, forKey: "age")
print(person4.valueForKey("age")!)
通过 setValue 方法,我们将 age 设置为 55,并在下一行代码中使用 valueForKey 将这个值打印出来。一切看似没什么不同。
那么假如我们又写了这一行语句呢:
person4.setValue(nil, forKey: "age")
额,你可以自己尝试一下,这时候程序会崩溃。原因嘛,很简单。 我们先来看 age 的定义:
var age: Int
age 是一个简单标量值(Int 整型变量),而标量值是不能够设置成 nil 的。虽然 KVC 提供给我们的 setValue 方法可以接受任何类型的参数作为值的设置,但 age 的底层存储确实标量值,因此我们执行上面那条 setValue 语句的时候必然会造成程序的崩溃。(这点在开发程序的时候确实需要格外留意,稍不留神可能就会浪费很多时间去调试错误)。
那么我们除了注意避免将 nil 传递给底层存储是标量类型的属性之外,还有没有其他方法呢? 答案是有的。
KVC 为我们提供了一个 setNilValueForKey 方法,每当我们要将 nil 设置给一个 key 的时候,这个方法就会被调用,所以我们可以修改一下 Person 类的定义:
class PersonForScalar : NSObject {
//...
override func setNilValueForKey(key: String) {
if key == "age" {
self.setValue(18, forKey: "age")
}
}
//...
}
我们在 setNilValueForKey 方法中,判断如果当前的 key 是 age 的话,就给它设置一个默认值 18。这次我们再次传入 nil 的时候,程序就不会因为抛出异常而崩溃,而是为这个 age 属性设置一个默认值。
集合属性
KVC 还提供了对集合属性的处理,简单来说就是这样,我们为 Person 类再添加一个 friends 属性,用于表示这个人的朋友:
class PersonForCollection : NSObject {
var firstName: String
var lastName: String
var friends: NSMutableArray
}
如果我们要为某一个 Person 的实例添加一个新朋友,或者获取它现有的朋友该怎么做呢? 大家可能会直接想到这样:
person5.friends.addObject(person6)
通过直接的属性引用,我们可以完成这样的需求。不过嘛,KVC 还给我们提供了专属的集合操作协议,这样我们就可以通过 KVC 的方式操作集合中的内容了,我们将 Person 类改写一下:
class PersonForCollection : NSObject {
var firstName: String
var lastName: String
var friends: NSMutableArray
init(firstName: String, lastName: String) {
self.firstName = firstName
self.lastName = lastName
self.friends = NSMutableArray()
}
func countOfFriends() -> Int {
return self.friends.count
}
func objectInFriendsAtIndex(index: Int) -> AnyObject? {
return self.friends[index]
}
}
这次我们新添加了两个方法,countOfFriends
和 objectInFriendsAtIndex
,这两个方法是 KVC 预定义的协议方法,用于集合类型的操作。注意这两个协议更明确的定义是这样 countOf<Key>
和 objectIn<Key>AtIndex
。 其中的 Key 代表集合操作的应的属性 key 的名字。比如 countOfFriends
, countOfAddress
, countOfBooks
这些都是合法的集合操作协议方法,前提是只要相应 key 值对应的属性存在。
那么集合操作方法定义好了,我们来看看如何使用 KVC 来操作集合属性吧:
person5.mutableArrayValueForKey("friends").count
这个调用取得当前的 friends 集合的 count 属性,这时候实际上调用了 countOfFriends
方法。自然,我们刚才还实现了 objectInFriendsAtIndex
方法,大家也能推理出这个方法如何使用了吧:
let friend = person5.mutableArrayValueForKey("friends")[0]
就是这样了,实际上 KVC 对于我们这个集合属性 friends
的操作都会通过 mutableArrayValueForKey 方法来进行,它会用我们传入的 key 值在当前实例中进行解析,如果接续成功会返回一个 NSMutableArray 类型的对象,我们就可以直接使用 NSMutableArray 的接口对集合类的属性进行操作了,不论他的底层存储是不是 NSMutableArray,它也是 NSKeyValueCoding 协议中定义的方法(这个协议定义我们在前面提到过,大家还记得吧~)。
我们刚才实现了集合相关的两个方法还缺了些什么呢 — 我们只实现了集合操作的 getter 方法,并没有实现 setter 方法。到目前,我们还不能通过 KVC 机制来给 firends 数组添加元素。
我们还需要添加两个方法:
class PersonForCollection : NSObject {
func insertObjectInFriendsAtIndex(friend: PersonForCollection, index: Int) {
self.friends.insertObject(friend, atIndex: index)
}
func removeObjectFromFriendsAtIndex(index: Int) {
self.friends.removeObjectAtIndex(index)
}
}
insertObjectInFriendsAtIndex
和 removeObjectFromFriendsAtIndex
分别用于向 friends 属性中插入元素和删除元素。现在我们也可以用 KVC 来操作集合内容了:
person5.mutableArrayValueForKey("friends").addObject(person6)
person5.mutableArrayValueForKey("friends").count
person5.mutableArrayValueForKey("friends").removeObjectAtIndex(0)
通过 KVC 的集合操作协议,我们实现了直接用 KVC 接口来操作集合属性的内容。 KVC 集合操作会更加灵活,friends 属性不一定是 NSMutableArray 类型, 它的底层存储可以是任何形式,只要我们实现了 KVC 集合操作接口,我们就能通过 KVC 像使用 NSMutableArray 一样来操作底层的集合了。
总结
好了,关于 KVC 咱们就说这么多,它还提供了很多其他非常好的特性,比如属性验证,可以通过这个方式来对属性的设置过程进行类似 filter 的操作。还提供了keyPath 的集合操作,比如我们通过这样一个 KeyPath 就可以获得 friends 集合的元素总数:
person5.valueForKeyPath("friends.@count")
善用 KVC 肯定会对我们的开发有很大的帮助。关于 KVC 如果大家想了解更多,推荐大家看一看苹果官方的文档 Key-Value Coding Programming Guide。
希望本篇文章的内容让大家再看了之后多多少少有些收货吧,我们下篇文章将会和大家一起探讨 KVO 的相关内容,也希望大家喜欢。
本篇内容相关代码的 playground 大家可以在 Github 上面找到: https://github.com/swiftcafex/kvc-kvo-samples