KVC 與 KVO 無疑是 Cocoa 提供給我們的一個(gè)非常強(qiáng)大的特性,使用熟練可以讓我們的代碼變得非常簡潔并且易讀。但 KVC 與 KVO 提供的 API 又是比較復(fù)雜的,絕對超出我們不經(jīng)深究之前所理解到的復(fù)雜度,這次大家就來跟我一起深入認(rèn)識這兩個(gè)特性吧。
基礎(chǔ)使用
首先,咱們要說的是 KVC (Key-Value Coding), 它是一種用間接方式訪問類的屬性的機(jī)制。在 Swift 中為一個(gè)類實(shí)現(xiàn) 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")!)
注意我們的兩個(gè) print 語句,第一個(gè)是使用直接引用屬性的方式,第二個(gè)就是使用 KVC 機(jī)制訪問的方式。 valueForKey 是 KVC 協(xié)議中定義的方法,它接受一個(gè)參數(shù),我們把它叫做 key,這個(gè) key 表示要訪問的屬性名稱,KVC 就會根據(jù)我們傳入的 key 幫助我們找到對應(yīng)的屬性。
不同之處
在 Swift 中處理 KVC和 Objective-C 中還是有些細(xì)微的差別。比如,Objective-C 中所有的類都繼承自 NSObject,而 Swift 中卻不是,所以我們在 Swift 中需要顯式的聲明繼承自 NSObject。
可為什么要繼承自 NSObject 呢?我們在蘋果官方的 KVC 文檔中找到了答案。其實(shí) KVC 機(jī)制是由一個(gè)協(xié)議 NSKeyValueCoding 定義的。NSObject 幫我們實(shí)現(xiàn)了這個(gè)協(xié)議,所以 KVC 核心的邏輯都在 NSObject 中,我們繼承 NSObject 才能讓我們的類獲得 KVC 的能力。(理論上說,如果你遵循 NSKeyValueCoding 協(xié)議的接口,其實(shí)也可以自己實(shí)現(xiàn) KVC 的細(xì)節(jié),完全行得通。但在實(shí)踐上,這么做就不太值得了,太費(fèi)時(shí)間了~)。
另外,因?yàn)?Swift 中的 Optional 機(jī)制,所以 valueForKey 方法返回的是一個(gè) Optional 值,我們還需要對返回值做一次解包處理,才能得到實(shí)際的屬性值。
關(guān)于 Optional 特性的內(nèi)容,可以參考這兩篇文章
淺談 Swift 中的 Optionals
關(guān)于 Optional 的一點(diǎn)嘮叨
那么書歸正傳,KVC 最主要的好處是什么呢,簡單來說就是我們可以不用過多的依賴編譯時(shí)的限制,而是為我們提供了更多的運(yùn)行時(shí)的能力。
valueForUndefinedKey
還是繼續(xù)咱們上面的例子,假如我們又寫了這樣一個(gè)語句會怎么樣呢:
peter.valueForKey("noExist")
因?yàn)槲覀兌x的 Person 類中是沒有 noExist 這個(gè)屬性的,所以 KVC 也無法找到這個(gè)屬性值,這時(shí)候 KVC 協(xié)議其實(shí)會調(diào)用 valueForUndefinedKey 方法,NSObject 對這個(gè)方法的默認(rèn)實(shí)現(xiàn)是拋出一個(gè) NSUndefinedKeyException 異常。所以如果我們沒有自己重寫 valueForUndefinedKey 方法的話,這時(shí)應(yīng)用就會因?yàn)楫惓1罎ⅰ?/p>
我們也可以在 Person 類中實(shí)現(xiàn)我們自己的 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 返回一個(gè)空字符串,這樣我們的 KVC 調(diào)用就能以更加優(yōu)雅的方式處理這個(gè)異常行為了。
valueForKeyPath
KVC 除了可以用單個(gè)的 key 來訪問單個(gè)屬性,還提供了一個(gè)叫做 keyPath 的東西。所謂 keyPath,就比如你的屬性本身也有自己的屬性,那么想引用這個(gè)屬性,就需要用到 keyPath。咱們用一個(gè)示例來說明:
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 類定義了一個(gè)屬性 address, 這個(gè) address 本身又是一個(gè)類,它也有兩個(gè)屬性 firstLine 和 lastLine, 那么我們?nèi)绻胍?address 的 firstLine 屬性,就可以使用 KVC 的 keyPath 機(jī)制:
print(peter3.valueForKeyPath("address.firstLine")!)
通過 keyPath,我們可以使用 KVC 將屬性引用范圍擴(kuò)大很多。這個(gè)規(guī)則對 Cocoa 系統(tǒng)類也適用,比如:
let view = UIView()
print(view.valueForKeyPath("superview.superview"))
我們可以通過 KVC 的這個(gè)機(jī)制遍歷 UIView 層級。
同樣的,如果 keyPath 中引用的任何一級屬性不存在或者不符合 KVC 規(guī)范, valueForUndefinedKey 方法就會被調(diào)用。
SetValueForKey
KVC 定義了使用 valueForKey 方法獲取屬性的值,同樣也提供了設(shè)置屬性值的方法,就是 setValue:forKey ", 還是接著上面的例子:
peter3.setValue("swift", forKey: "firstName")
print(peter3.valueForKey("firstName")!)
setValue:forKey 方法接受兩個(gè)參數(shù),第一個(gè)參數(shù)是我們要設(shè)置的屬性的值,第二個(gè)參數(shù)是屬性的 key。這個(gè)接口很簡單明了,就不多贅述了。
和 valueForKey 一樣,如果我們給 setValue 傳遞一個(gè)不存在的 key 值,KVC 就會去調(diào)用 setValue: forUndefinedKey 方法,NSObject 對這個(gè)方法的默認(rèn)實(shí)現(xiàn)依然是拋出一個(gè) NSUndefinedKeyException 異常。
關(guān)于標(biāo)量值
所謂標(biāo)量值(Scalar Type),指的是簡單類型的屬性,比如 int,float 這些非對象的屬性。關(guān)于標(biāo)量值的在 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
}
}
那么現(xiàn)在可以使用 KVC 來操作它的各個(gè)屬性:
var person4 = PersonForScalar(firstName: "peter", lastName: "cook", age: 32)
person4.setValue(55, forKey: "age")
print(person4.valueForKey("age")!)
通過 setValue 方法,我們將 age 設(shè)置為 55,并在下一行代碼中使用 valueForKey 將這個(gè)值打印出來。一切看似沒什么不同。
那么假如我們又寫了這一行語句呢:
person4.setValue(nil, forKey: "age")
額,你可以自己嘗試一下,這時(shí)候程序會崩潰。原因嘛,很簡單。 我們先來看 age 的定義:
var age: Int
age 是一個(gè)簡單標(biāo)量值(Int 整型變量),而標(biāo)量值是不能夠設(shè)置成 nil 的。雖然 KVC 提供給我們的 setValue 方法可以接受任何類型的參數(shù)作為值的設(shè)置,但 age 的底層存儲確實(shí)標(biāo)量值,因此我們執(zhí)行上面那條 setValue 語句的時(shí)候必然會造成程序的崩潰。(這點(diǎn)在開發(fā)程序的時(shí)候確實(shí)需要格外留意,稍不留神可能就會浪費(fèi)很多時(shí)間去調(diào)試錯(cuò)誤)。
那么我們除了注意避免將 nil 傳遞給底層存儲是標(biāo)量類型的屬性之外,還有沒有其他方法呢? 答案是有的。
KVC 為我們提供了一個(gè) setNilValueForKey 方法,每當(dāng)我們要將 nil 設(shè)置給一個(gè) key 的時(shí)候,這個(gè)方法就會被調(diào)用,所以我們可以修改一下 Person 類的定義:
class PersonForScalar : NSObject {
//...
override func setNilValueForKey(key: String) {
if key == "age" {
self.setValue(18, forKey: "age")
}
}
//...
}
我們在 setNilValueForKey 方法中,判斷如果當(dāng)前的 key 是 age 的話,就給它設(shè)置一個(gè)默認(rèn)值 18。這次我們再次傳入 nil 的時(shí)候,程序就不會因?yàn)閽伋霎惓6罎?,而是為這個(gè) age 屬性設(shè)置一個(gè)默認(rèn)值。
集合屬性
KVC 還提供了對集合屬性的處理,簡單來說就是這樣,我們?yōu)?Person 類再添加一個(gè) friends 屬性,用于表示這個(gè)人的朋友:
class PersonForCollection : NSObject {
var firstName: String
var lastName: String
var friends: NSMutableArray
}
如果我們要為某一個(gè) Person 的實(shí)例添加一個(gè)新朋友,或者獲取它現(xiàn)有的朋友該怎么做呢? 大家可能會直接想到這樣:
person5.friends.addObject(person6)
通過直接的屬性引用,我們可以完成這樣的需求。不過嘛,KVC 還給我們提供了專屬的集合操作協(xié)議,這樣我們就可以通過 KVC 的方式操作集合中的內(nèi)容了,我們將 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]
}
}
這次我們新添加了兩個(gè)方法,countOfFriends 和 objectInFriendsAtIndex ,這兩個(gè)方法是 KVC 預(yù)定義的協(xié)議方法,用于集合類型的操作。注意這兩個(gè)協(xié)議更明確的定義是這樣 countOf<Key> 和 objectIn<Key>AtIndex。 其中的 Key 代表集合操作的應(yīng)的屬性 key 的名字。比如 countOfFriends, countOfAddress, countOfBooks 這些都是合法的集合操作協(xié)議方法,前提是只要相應(yīng) key 值對應(yīng)的屬性存在。
那么集合操作方法定義好了,我們來看看如何使用 KVC 來操作集合屬性吧:
person5.mutableArrayValueForKey("friends").count
這個(gè)調(diào)用取得當(dāng)前的 friends 集合的 count 屬性,這時(shí)候?qū)嶋H上調(diào)用了 countOfFriends 方法。自然,我們剛才還實(shí)現(xiàn)了 objectInFriendsAtIndex 方法,大家也能推理出這個(gè)方法如何使用了吧:
let friend = person5.mutableArrayValueForKey("friends")[0]
就是這樣了,實(shí)際上 KVC 對于我們這個(gè)集合屬性 friends 的操作都會通過 mutableArrayValueForKey 方法來進(jìn)行,它會用我們傳入的 key 值在當(dāng)前實(shí)例中進(jìn)行解析,如果接續(xù)成功會返回一個(gè) NSMutableArray 類型的對象,我們就可以直接使用 NSMutableArray 的接口對集合類的屬性進(jìn)行操作了,不論他的底層存儲是不是 NSMutableArray,它也是 NSKeyValueCoding 協(xié)議中定義的方法(這個(gè)協(xié)議定義我們在前面提到過,大家還記得吧~)。
我們剛才實(shí)現(xiàn)了集合相關(guān)的兩個(gè)方法還缺了些什么呢 — 我們只實(shí)現(xiàn)了集合操作的 getter 方法,并沒有實(shí)現(xiàn) setter 方法。到目前,我們還不能通過 KVC 機(jī)制來給 firends 數(shù)組添加元素。
我們還需要添加兩個(gè)方法:
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 屬性中插入元素和刪除元素?,F(xiàn)在我們也可以用 KVC 來操作集合內(nèi)容了:
person5.mutableArrayValueForKey("friends").addObject(person6)
person5.mutableArrayValueForKey("friends").count
person5.mutableArrayValueForKey("friends").removeObjectAtIndex(0)
通過 KVC 的集合操作協(xié)議,我們實(shí)現(xiàn)了直接用 KVC 接口來操作集合屬性的內(nèi)容。 KVC 集合操作會更加靈活,friends 屬性不一定是 NSMutableArray 類型, 它的底層存儲可以是任何形式,只要我們實(shí)現(xiàn)了 KVC 集合操作接口,我們就能通過 KVC 像使用 NSMutableArray 一樣來操作底層的集合了。
總結(jié)
好了,關(guān)于 KVC 咱們就說這么多,它還提供了很多其他非常好的特性,比如屬性驗(yàn)證,可以通過這個(gè)方式來對屬性的設(shè)置過程進(jìn)行類似 filter 的操作。還提供了keyPath 的集合操作,比如我們通過這樣一個(gè) KeyPath 就可以獲得 friends 集合的元素總數(shù):
person5.valueForKeyPath("friends.@count")
善用 KVC 肯定會對我們的開發(fā)有很大的幫助。關(guān)于 KVC 如果大家想了解更多,推薦大家看一看蘋果官方的文檔 Key-Value Coding Programming Guide。
希望本篇文章的內(nèi)容讓大家再看了之后多多少少有些收貨吧,我們下篇文章將會和大家一起探討 KVO 的相關(guān)內(nèi)容,也希望大家喜歡。
本篇內(nèi)容相關(guān)代碼的 playground 大家可以在 Github 上面找到: https://github.com/swiftcafex/kvc-kvo-samples
更多精彩內(nèi)容可關(guān)注微信公眾號:
swift-cafe
