Key-value coding (KVC) 和 key-value observing (KVO) 是兩種能讓我們駕馭 Objective-C 動態特性並簡化程式碼的機制。在這篇文章裡,我們將接觸一些如何利用這些特性的例子。
觀察 model 物件的變化
在 Cocoa 的模型-檢視-控制器 (Model-view-controller)架構裡,控制器負責讓檢視和模型同步。這一共有兩步:當 model 物件改變的時候,檢視應該隨之改變以反映模型的變化;當使用者和控制器互動的時候,模型也應該做出相應的改變。
KVO 能幫助我們讓檢視和模型保持同步。控制器可以觀察檢視依賴的屬性變化。
讓我們看一個例子:我們的模型類 LabColor
代表一種 Lab色彩空間裡的顏色。和 RGB 不同,這種色彩空間有三個元素 L, a, b。我們要做一個用來改變這些值的滑塊和一個顯示顏色的方塊區域。
我們的模型類有以下三個用來代表顏色的屬性:
1 2 3 |
@property (nonatomic) double lComponent; @property (nonatomic) double aComponent; @property (nonatomic) double bComponent; |
依賴的屬性
我們需要從這個類建立一個 UIColor
物件來顯示出顏色。我們新增三個額外的屬性,分別對應 R, G, B:
1 2 3 4 5 |
@property (nonatomic, readonly) double redComponent; @property (nonatomic, readonly) double greenComponent; @property (nonatomic, readonly) double blueComponent; @property (nonatomic, strong, readonly) UIColor *color; |
有了這些以後,我們就可以建立這個類的介面了:
1 2 3 4 5 6 7 8 9 10 11 12 |
@interface LabColor : NSObject @property (nonatomic) double lComponent; @property (nonatomic) double aComponent; @property (nonatomic) double bComponent; @property (nonatomic, readonly) double redComponent; @property (nonatomic, readonly) double greenComponent; @property (nonatomic, readonly) double blueComponent; @property (nonatomic, strong, readonly) UIColor *color; @end |
維基百科提供了轉換 RGB 到 Lab 色彩空間的演算法。寫成方法之後如下所示:
1 2 3 4 5 6 7 8 9 10 11 |
- (double)greenComponent; { return D65TristimulusValues[1] * inverseF(1./116. * (self.lComponent + 16) + 1./500. * self.aComponent); } [...] - (UIColor *)color { return [UIColor colorWithRed:self.redComponent * 0.01 green:self.greenComponent * 0.01 blue:self.blueComponent * 0.01 alpha:1.]; } |
這些程式碼沒什麼令人激動的地方。有趣的是 greenComponent
屬性依賴於 lComponent
和 aComponent
。不論何時設定 lComponent
的值,我們需要讓 RGB 三個 component 中與其相關的成員以及 color
屬性都要得到通知以保持一致。這一點這在 KVO 中很重要。
Foundation 框架提供的表示屬性依賴的機制如下:
1 |
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key |
更詳細的如下:
1 |
+ (NSSet *)keyPathsForValuesAffecting<鍵名> |
在我們的例子中如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
+ (NSSet *)keyPathsForValuesAffectingRedComponent { return [NSSet setWithObject:@"lComponent"]; } + (NSSet *)keyPathsForValuesAffectingGreenComponent { return [NSSet setWithObjects:@"lComponent", @"aComponent", nil]; } + (NSSet *)keyPathsForValuesAffectingBlueComponent { return [NSSet setWithObjects:@"lComponent", @"bComponent", nil]; } + (NSSet *)keyPathsForValuesAffectingColor { return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil]; } |
現在我們完整的表達了屬性之間的依賴關係。請注意,我們可以把這些屬性連結起來。打個比方,如果我們寫一個子類去 overrideredComponent
方法,這些依賴關係仍然能正常工作。
觀察變化
現在讓我們目光轉向控制器。 NSViewController
的子類擁有 LabColor
model 物件作為其屬性。
1 2 3 4 5 |
@interface ViewController () @property (nonatomic, strong) LabColor *labColor; @end |
我們把檢視控制器註冊為觀察者來接收 KVO 的通知,這可以用以下 NSObject
的方法來實現:
1 2 3 4 |
- (void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context |
這會讓以下方法:
1 2 3 4 |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context |
在當 keyPath
的值改變的時候在觀察者 anObserver
上面被呼叫。這個 API 看起來有一點嚇人。更糟糕的是,我們還得記得呼叫以下的方法
1 2 |
- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath |
來移除觀察者,否則我們我們的 app 會因為某些奇怪的原因崩潰。
對於大多數的應用來說,KVO 可以通過輔助類用一種更簡單優雅的方式實現。我們在檢視控制器新增以下的觀察記號(Observation token)屬性:
1 |
@property (nonatomic, strong) id colorObserveToken; |
當 labColor
在檢視控制器中被設定時,我們只要 override labColor
的 setter 方法就行了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (void)setLabColor:(LabColor *)labColor { _labColor = labColor; self.colorObserveToken = [KeyValueObserver observeObject:labColor keyPath:@"color" target:self selector:@selector(colorDidChange:) options:NSKeyValueObservingOptionInitial]; } - (void)colorDidChange:(NSDictionary *)change; { self.colorView.backgroundColor = self.labColor.color; } |
KeyValueObserver
輔助類 封裝了 -addObserver:forKeyPath:options:context:
,-observeValueForKeyPath:ofObject:change:context:
和-removeObserverForKeyPath:
的呼叫,讓檢視控制器遠離雜亂的程式碼。
整合到一起
檢視控制器需要對 L,a,b 的滑塊控制做出反應:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (IBAction)updateLComponent:(UISlider *)sender; { self.labColor.lComponent = sender.value; } - (IBAction)updateAComponent:(UISlider *)sender; { self.labColor.aComponent = sender.value; } - (IBAction)updateBComponent:(UISlider *)sender; { self.labColor.bComponent = sender.value; } |
所有的程式碼都在我們的 GitHub 示例程式碼 中找到。
手動通知 vs 自動通知
我們剛才所做的事情有點神奇,但是實際上發生的事情是,當 LabColor
例項的 -setLComponent:
等方法被呼叫的時候以下方法:
1 |
- (void)willChangeValueForKey:(NSString *)key |
和:
1 |
- (void)didChangeValueForKey:(NSString *)key |
會在執行 -setLComponent:
中的程式碼之前以及之後被自動呼叫。如果我們寫了 -setLComponent:
或者我們選擇使用自動 synthesize 的 lComponent
的 accessor 到時候就會發生這樣的事情。
有些情況下當我們需要 override -setLComponent:
並且我們要控制是否傳送鍵值改變的通知的時候,我們要做以下的事情:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
+ (BOOL)automaticallyNotifiesObserversForLComponent; { return NO; } - (void)setLComponent:(double)lComponent; { if (_lComponent == lComponent) { return; } [self willChangeValueForKey:@"lComponent"]; _lComponent = lComponent; [self didChangeValueForKey:@"lComponent"]; } |
我們關閉了 -willChangeValueForKey:
和 -didChangeValueForKey:
的自動呼叫,然後我們手動呼叫他們。我們只應該在關閉了自動呼叫的時候我們才需要在 setter 方法裡手動呼叫 -willChangeValueForKey:
和 -didChangeValueForKey:
。大多數情況下,這樣優化不會給我們帶來太多好處。
如果我們在 accessor 方法之外改變例項物件(如 _lComponent
),我們要特別小心地和剛才一樣封裝 -willChangeValueForKey:
和 -didChangeValueForKey:
。不過在多數情況下,我們只用 accessor 方法的話就可以了,這樣程式碼會簡潔很多。
KVO 和 context
有時我們會有理由不想用 KeyValueObserver
輔助類。建立另一個物件會有額外的效能開銷。如果我們觀察很多個鍵的話,這個開銷可能會變得明顯。
如果我們在實現一個類的時候把它自己註冊為觀察者的話:
1 2 3 4 |
- (void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context |
一個非常重要的點是我們要傳入一個這個類唯一的 context
。我們推薦把以下程式碼
1 |
static int const PrivateKVOContext; |
寫在這個類 .m
檔案的頂端,然後我們像這樣呼叫 API 並傳入 PrivateKVOContext
的指標:
1 |
[otherObject addObserver:self forKeyPath:@"someKey" options:someOptions context:&PrivateKVOContext]; |
然後我們這樣寫 -observeValueForKeyPath:...
的方法:
1 2 3 4 5 6 7 8 9 10 11 |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == &PrivateKVOContext) { // 這裡寫相關的觀察程式碼 } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } |
這將確保我們寫的子類都是正確的。如此一來,子類和父類都能安全的觀察同樣的鍵值而不會衝突。否則我們將會碰到難以 debug 的奇怪行為。
進階 KVO
我們常常需要當一個值改變的時候更新 UI,但是我們也要在第一次執行程式碼的時候更新一次 UI。我們可以用 KVO 並新增NSKeyValueObservingOptionInitial
的選項 來一箭雙鵰地做好這樣的事情。這將會讓 KVO 通知在呼叫 -addObserver:forKeyPath:...
到時候也被觸發。
之前和之後
當我們註冊 KVO 通知的時候,我們可以新增 NSKeyValueObservingOptionPrior
選項,這能使我們在鍵值改變之前被通知。這和-willChangeValueForKey:
被觸發的時間相對應。
如果我們註冊通知的時候附加了 NSKeyValueObservingOptionPrior
選項,我們將會收到兩個通知:一個在值變更前,另一個在變更之後。變更前的通知將會在 change
字典中有不同的鍵。我們可以像以下這樣區分通知是在改變之前還是之後被觸發的:
1 2 3 4 5 |
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) { // 改變之前 } else { // 改變之後 } |
值
如果我們需要改變前後的值,我們可以在 KVO 選項中加入 NSKeyValueObservingOptionNew
和/或NSKeyValueObservingOptionOld
。
更簡單的辦法是用 NSKeyValueObservingOptionPrior
選項,隨後我們就可以用以下方式提取出改變前後的值:
1 2 |
id oldValue = change[NSKeyValueChangeOldKey]; id newValue = change[NSKeyValueChangeNewKey]; |
通常來說 KVO 會在 -willChangeValueForKey:
和 -didChangeValueForKey:
被呼叫的時候儲存相應鍵的值。
索引
KVO 對一些集合類也有很強的支援,以下方法會返回集合物件:
1 2 3 |
-mutableArrayValueForKey: -mutableSetValueForKey: -mutableOrderedSetValueForKey: |
我們將會詳細解釋這是怎麼工作的。如果你使用這些方法,change 字典裡會包含鍵值變化的型別(新增、刪除和替換)。對於有序的集合,change 字典會包含受影響的 index。
集合代理物件和變化的通知在用於更新UI的時候非常有效,尤其是處理大集合的時候。但是它們需要花費你一些心思。
KVO 和執行緒
一個需要注意的地方是,KVO 行為是同步的,並且發生與所觀察的值發生變化的同樣的執行緒上。沒有佇列或者 Run-loop 的處理。手動或者自動呼叫 -didChange...
會觸發 KVO 通知。
所以,當我們試圖從其他執行緒改變屬性值的時候我們應當十分小心,除非能確定所有的觀察者都用執行緒安全的方法處理 KVO 通知。通常來說,我們不推薦把 KVO 和多執行緒混起來。如果我們要用多個佇列和執行緒,我們不應該在它們互相之間用 KVO。
KVO 是同步執行的這個特性非常強大,只要我們在單一執行緒上面執行(比如主佇列 main queue),KVO 會保證下列兩種情況的發生:
首先,如果我們呼叫一個支援 KVO 的 setter 方法,如下所示:
1 |
self.exchangeRate = 2.345; |
KVO 能保證所有 exchangeRate
的觀察者在 setter 方法返回前被通知到。
其次,如果某個鍵被觀察的時候附上了 NSKeyValueObservingOptionPrior
選項,直到 -observe...
被呼叫之前,exchangeRate
的 accessor 方法都會返回同樣的值。
KVC
最簡單的 KVC 能讓我們通過以下的形式訪問屬性:
1 |
@property (nonatomic, copy) NSString *name; |
取值:
1 |
NSString *n = [object valueForKey:@"name"] |
設定:
1 |
[object setValue:@"Daniel" forKey:@"name"] |
值得注意的是這個不僅可以訪問作為物件屬性,而且也能訪問一些標量(例如 int
和 CGFloat
)和 struct(例如 CGRect
)。Foundation 框架會為我們自動封裝它們。舉例來說,如果有以下屬性:
1 |
@property (nonatomic) CGFloat height; |
我們可以這樣設定它:
1 |
[object setValue:@(20) forKey:@"height"] |
KVC 允許我們用屬性的字串名稱來訪問屬性,字串在這兒叫做鍵。有些情況下,這會使我們非常靈活地簡化程式碼。我們下一節介紹例子簡化列表 UI。
KVC 還有更多可以談的。集合(NSArray
,NSSet
等)結合 KVC 可以擁有一些強大的集合操作。還有,物件可以支援用 KVC 通過代理物件訪問非常規的屬性。
簡化列表 UI
假設我們有這樣一個物件:
1 2 3 4 5 6 7 8 |
@interface Contact : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *nickname; @property (nonatomic, copy) NSString *email; @property (nonatomic, copy) NSString *city; @end |
還有一個 detail 檢視控制器,含有四個對應的 UITextField
屬性:
1 2 3 4 5 6 7 8 |
@interface DetailViewController () @property (weak, nonatomic) IBOutlet UITextField *nameField; @property (weak, nonatomic) IBOutlet UITextField *nicknameField; @property (weak, nonatomic) IBOutlet UITextField *emailField; @property (weak, nonatomic) IBOutlet UITextField *cityField; @end |
我們可以簡化更新 UI 的邏輯。首先我們需要兩個方法:一個返回 model 裡我們用到的所有鍵的方法,一個把鍵對映到對應的文字框的方法:
1 2 3 4 5 6 7 8 9 |
- (NSArray *)contactStringKeys; { return @[@"name", @"nickname", @"email", @"city"]; } - (UITextField *)textFieldForModelKey:(NSString *)key; { return [self valueForKey:[key stringByAppendingString:@"Field"]]; } |
有了這個,我們可以從 model 裡更新文字框,如下所示:
1 2 3 4 5 6 |
- (void)updateTextFields; { for (NSString *key in self.contactStringKeys) { [self textFieldForModelKey:key].text = [self.contact valueForKey:key]; } } |
我們也可以用一個 action 方法讓四個文字框都能實時更新 model:
1 2 3 4 5 6 7 8 9 10 |
- (IBAction)fieldEditingDidEnd:(UITextField *)sender { for (NSString *key in self.contactStringKeys) { UITextField *field = [self textFieldForModelKey:key]; if (field == sender) { [self.contact setValue:sender.text forKey:key]; break; } } } |
注意:我們之後會新增驗證輸入的部分,在鍵值驗證裡會提到。
最後,我們需要確認文字框在需要的時候被更新:
1 2 3 4 5 6 7 8 9 10 11 |
- (void)viewWillAppear:(BOOL)animated; { [super viewWillAppear:animated]; [self updateTextFields]; } - (void)setContact:(Contact *)contact { _contact = contact; [self updateTextFields]; } |
有了這個,我們的 detail 檢視控制器 就能正常工作了。
整個專案可以在 GitHub 上找到。它也用了我們後面提到的鍵值驗證。
鍵路徑(Key Path)
KVC 同樣允許我們通過關係來訪問物件。假設 person
物件有屬性 address
,address
有屬性 city
,我們可以這樣通過 person
來訪問 city
:
1 |
[person valueForKeyPath:@"address.city"] |
值得注意的是這裡我們呼叫 -valueForKeyPath:
而不是 -valueForKey:
。
Key-Value Coding Without @property
不需要 @property
的 KVC
我們可以實現一個支援 KVC 而不用 @property
和 @synthesize
或是自動 synthesize 的屬性。最直接的方式是新增 -<key>
和 -set<Key>:
方法。例如我們想要 name
,我們這樣做:
1 2 |
- (NSString *)name; - (void)setName:(NSString *)name; |
這完全等於 @property
的實現方式。
但是當標量和 struct 的值被傳入 nil
的時候尤其需要注意。假設我們要 height
屬性支援 KVC 我們寫了以下的方法:
1 2 |
- (CGFloat)height; - (void)setHeight:(CGFloat)height; |
然後我們這樣呼叫:
1 |
[object setValue:nil forKey:@"height"] |
這會丟擲一個 exception。要正確的處理 nil
,我們要像這樣 override -setNilValueForKey:
1 2 3 4 5 6 7 |
- (void)setNilValueForKey:(NSString *)key { if ([key isEqualToString:@"height"]) { [self setValue:@0 forKey:key]; } else [super setNilValueForKey:key]; } |
我們可以通過 override 這些方法來讓一個類支援 KVC:
1 2 |
- (id)valueForUndefinedKey:(NSString *)key; - (void)setValue:(id)value forUndefinedKey:(NSString *)key; |
這也許看起來很怪,但這可以讓一個類動態的支援一些鍵的訪問。但是這兩個方法會在效能上拖後腿。
附註:Foundation 框架支援直接訪問例項變數。請小心的使用這個特性。你可以去檢視 +accessInstanceVariablesDirectly
的文件。這個值預設是 YES
的時候,Foundation 會按照 _<key>
, _is<Key>
, <key>
和 is<Key>
的順序查詢例項變數。
集合的操作
一個常常被忽視的 KVC 特性是它對集合操作的支援。舉個例子,我們可以這樣來獲得一個陣列中最大的值:
1 2 |
NSArray *a = @[@4, @84, @2]; NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]); |
或者說,我們有一個 Transaction
物件的陣列,物件有屬性 amount
的話,我們可以這樣獲得最大的 amount
:
1 2 |
NSArray *a = @[transaction1, transaction2, transaction3]; NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]); |
當我們呼叫 [a valueForKeyPath:@"@max.amount"]
的時候,它會在陣列 a
的每個元素中呼叫 -valueForKey:@"amount"
然後返回最大的那個。
KVC 的蘋果官方文件有一個章節 Collection Operators 詳細的講述了類似的用法。
通過集合代理物件來實現 KVC
雖然我們可以像對待一般的物件一樣用 KVC 深入集合內部(NSArray
和 NSSet
等),但是通過集合代理物件, KVC 也讓我們實現一個相容 KVC 的集合。這是一個頗為高階的技巧。
當我們在物件上呼叫 -valueForKey:
的時候,它可以返回 NSArray
,NSSet
或是 NSOrderedSet
的集合代理物件。這個類沒有實現通常的 -<Key>
方法,但是它實現了代理物件所需要使用的很多方法。
如果我們希望一個類支援通過代理物件的 contacts
鍵返回一個 NSArray
,我們可以這樣寫:
1 2 |
- (NSUInteger)countOfContacts; - (id)objectInContactsAtIndex:(NSUInteger)idx; |
這樣做的話,當我們呼叫 [object valueForKey:@"contacts”]
的時候,它會返回一個由這兩個方法來代理所有呼叫方法的NSArray
物件。這個陣列支援所有正常的對 NSArray
的呼叫。換句話說,呼叫者並不知道返回的是一個真正的 NSArray
, 還是一個代理的陣列。
對於 NSSet
和 NSOrderedSet
,如果要做同樣的事情,我們需要實現的方法是:
NSArray | NSSet | NSOrderedSet |
---|---|---|
-countOf<Key> |
-countOf<Key> |
-countOf<Key> |
-enumeratorOf<Key> |
-indexIn<Key>OfObject: |
|
以下兩者二選一 | -memberOf<Key>: |
|
-objectIn<Key>AtIndex: |
以下兩者二選一 | |
-<key>AtIndexes: |
-objectIn<Key>AtIndex: |
|
-<key>AtIndexes: |
||
可選(增強效能) | ||
-get<Key>:range: |
可選(增強效能) | |
-get<Key>:range: |
可選 的一些方法可以增強代理物件的效能。
雖然只有特殊情況下我們用這些代理物件才會有意義,但是在這些情況下代理物件非常的有用。想象一下我們有一個很大的資料結構,呼叫者不需要(一次性)訪問所有的物件。
舉一個(也許比較做作的)例子說,我們想寫一個包含有很長一串質數的類。如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
@interface Primes : NSObject @property (readonly, nonatomic, strong) NSArray *primes; @end @implementation Primes static int32_t const primes[] = { 2, 101, 233, 383, 3, 103, 239, 389, 5, 107, 241, 397, 7, 109, 251, 401, 11, 113, 257, 409, 13, 127, 263, 419, 17, 131, 269, 421, 19, 137, 271, 431, 23, 139, 277, 433, 29, 149, 281, 439, 31, 151, 283, 443, 37, 157, 293, 449, 41, 163, 307, 457, 43, 167, 311, 461, 47, 173, 313, 463, 53, 179, 317, 467, 59, 181, 331, 479, 61, 191, 337, 487, 67, 193, 347, 491, 71, 197, 349, 499, 73, 199, 353, 503, 79, 211, 359, 509, 83, 223, 367, 521, 89, 227, 373, 523, 97, 229, 379, 541, 547, 701, 877, 1049, 557, 709, 881, 1051, 563, 719, 883, 1061, 569, 727, 887, 1063, 571, 733, 907, 1069, 577, 739, 911, 1087, 587, 743, 919, 1091, 593, 751, 929, 1093, 599, 757, 937, 1097, 601, 761, 941, 1103, 607, 769, 947, 1109, 613, 773, 953, 1117, 617, 787, 967, 1123, 619, 797, 971, 1129, 631, 809, 977, 1151, 641, 811, 983, 1153, 643, 821, 991, 1163, 647, 823, 997, 1171, 653, 827, 1009, 1181, 659, 829, 1013, 1187, 661, 839, 1019, 1193, 673, 853, 1021, 1201, 677, 857, 1031, 1213, 683, 859, 1033, 1217, 691, 863, 1039, 1223, 1229, }; - (NSUInteger)countOfPrimes; { return (sizeof(primes) / sizeof(*primes)); } - (id)objectInPrimesAtIndex:(NSUInteger)idx; { NSParameterAssert(idx < sizeof(primes) / sizeof(*primes)); return @(primes[idx]); } @end |
我們將會執行以下程式碼:
1 2 |
Primes *primes = [[Primes alloc] init]; NSLog(@"The last prime is %@", [primes.primes lastObject]); |
這將會呼叫一次 -countOfPrimes
和一次傳入引數 idx
作為最後一個索引的 -objectInPrimesAtIndex:
。為了只取出最後一個值,它不需要先把所有的數封裝成 NSNumber
然後把它們都匯入 NSArray
。
在一個複雜一點的例子中,通訊錄編輯器示例 app 用同樣的方法把 C++ std::vector
封裝以來。它詳細說明了應該怎麼利用這個方法。
可變的集合
我們也可以在可變集合(例如 NSMutableArray
,NSMutableSet
,和 NSMutableOrderedSet
)中用集合代理。
訪問這些可變的集合有一點點不同。呼叫者在這兒需要呼叫以下其中一個方法:
1 2 3 |
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key; - (NSMutableSet *)mutableSetValueForKey:(NSString *)key; - (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key; |
一個竅門:我們可以讓一個類用以下方法返回可變集合的代理:
1 2 3 4 |
- (NSMutableArray *)mutableContacts; { return [self mutableArrayValueForKey:@"wrappedContacts"]; } |
然後在實現鍵 wrappedContacts
的一些方法。
我們需要實現上面的不變集合的兩個方法,還有以下的幾個:
NSMutableArray / NSMutableOrderedSet | NSMutableSet |
---|---|
至少實現一個插入方法和一個刪除方法 | 至少實現一個插入方法和一個刪除方法 |
-insertObject:in<Key>AtIndex: |
-add<Key>Object: |
-removeObjectFrom<Key>AtIndex: |
-remove<Key>Object: |
-insert<Key>:atIndexes: |
-add<Key>: |
-remove<Key>AtIndexes: |
-remove<Key>: |
可選(增強效能)以下方法二選一 | 可選(增強效能) |
-replaceObjectIn<Key>AtIndex:withObject: |
-intersect<Key>: |
-replace<Key>AtIndexes:with<Key>: |
-set<Key>: |
上面提到,這些可變集合代理物件和 KVO 結合起來也十分強大。KVO 機制能在這些集合改變的時候把詳細的變化放進 change 字典中。
有批量更新(需要傳入多個物件)的方法,也有隻改變一個物件的方法。我們推薦選擇相對於給定任務來說最容易實現的那個來寫,雖然我們有一點點傾向於選擇批量更新的那個。
在實現這些方法的時候,我們要對自動和手動的 KVO 之間的差別十分小心。Foundation 預設自動發出十分詳盡的變化通知。如果我們要手動實現傳送詳細通知的話,我們得實現這些:
1 2 |
-willChange:valuesAtIndexes:forKey: -didChange:valuesAtIndexes:forKey: |
或者這些:
1 2 |
-willChangeValueForKey:withSetMutation:usingObjects: -didChangeValueForKey:withSetMutation:usingObjects: |
我們要保證先把自動通知關閉,否則每次改變 KVO 都會發出兩次通知。
常見的 KVO 錯誤
首先,KVO 相容是 API 的一部分。如果類的所有者不保證某個屬性相容 KVO,我們就不能保證 KVO 正常工作。蘋果文件裡有 KVO 相容屬性的文件。例如,NSProgress
類的大多數屬性都是相容 KVO 的。
當做出改變以後,有些人試著放空的 -willChange
和 -didChange
方法來強制 KVO 的觸發。KVO 通知雖然會生效,但是這樣做破壞了有依賴於 NSKeyValueObservingOld
選項的觀察者。詳細來說,這影響了 KVO 對觀察鍵路徑 (key path) 的原生支援。KVO 在觀察鍵路徑 (key path) 時依賴於 NSKeyValueObservingOld
屬性。
我們也要指出有些集合是不能被觀察的。KVO 旨在觀察關係 (relationship) 而不是集合。我們不能觀察 NSArray
,我們只能觀察一個物件的屬性——而這個屬性有可能是 NSArray
。舉例說,如果我們有一個 ContactList
物件,我們可以觀察它的 contacts
屬性。但是我們不能向要觀察物件的 -addObserver:forKeyPath:...
傳入一個 NSArray
。
相似地,觀察 self
不是永遠都生效的。而且這不是一個好的設計。
除錯 KVO
你可以在 lldb
裡檢視一個被觀察物件的所有觀察資訊。
1 |
(lldb) po [observedObject observationInfo] |
這會列印出有關誰觀察誰之類的很多資訊。
這個資訊的格式不是公開的,我們不能讓任何東西依賴它,因為蘋果隨時都可以改變它。不過這是一個很強大的排錯工具。
鍵值驗證 (KVV)
最後提示,KVV 也是 KVC API 的一部分。這是一個用來驗證屬性值的 API,只是它光靠自己很難提供邏輯和功能。
如果我們寫能夠驗證值的 model 類的話,我們就應該實現 KVV 的 API 來保證一致性。用 KVV 驗證 model 類的值是 Cocoa 的慣例。
讓我們在一次強調一下:KVC 不會做任何的驗證,也不會呼叫任何 KVV 的方法。那是你的控制器需要做的事情。通過 KVV 實現你自己的驗證方法會保證它們的一致性。
以下是一個簡單的例子:
1 2 3 4 5 6 7 8 9 10 11 |
- (IBAction)nameFieldEditingDidEnd:(UITextField *)sender; { NSString *name = [sender text]; NSError *error = nil; if ([self.contact validateName:&name error:&error]) { self.contact.name = name; } else { // Present the error to the user } sender.text = self.contact.name; } |
它強大之處在於,當 model 類(Contact
)驗證 name
的時候,會有機會去處理名字。
如果我們想讓名字不要有前後的空白字元,我們應該把這些邏輯放在 model 物件裡面。Contact
類可以像這樣實現 KVV:
1 2 3 4 5 6 7 8 9 10 |
- (BOOL)validateName:(NSString **)nameP error:(NSError * __autoreleasing *)error { if (*nameP == nil) { *nameP = @""; return YES; } else { *nameP = [*nameP stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; return YES; } } |
通訊錄示例 裡的 DetailViewController
和 Contact
類詳解了這個用法。