Key-value coding (KVC) 和 key-value observing (KVO) 是兩種能讓咱們駕馭 Objective-C 動態特性並簡化代碼的機制。在這篇文章裏,咱們將接觸一些如何利用這些特性的例子。html
在 Cocoa 的模型-視圖-控制器 (Model-view-controller)架構裏,控制器負責讓視圖和模型同步。這一共有兩步:當 model 對象改變的時候,視圖應該隨之改變以反映模型的變化;當用戶和控制器交互的時候,模型也應該作出相應的改變。ios
KVO 能幫助咱們讓視圖和模型保持同步。控制器能夠觀察視圖依賴的屬性變化。git
讓咱們看一個例子:咱們的模型類 LabColor
表明一種 Lab色彩空間裏的顏色。和 RGB 不一樣,這種色彩空間有三個元素 L, a, b。咱們要作一個用來改變這些值的滑塊和一個顯示顏色的方塊區域。github
咱們的模型類有如下三個用來表明顏色的屬性:算法
@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;
複製代碼
咱們須要從這個類建立一個 UIColor
對象來顯示出顏色。咱們添加三個額外的屬性,分別對應 R, G, B:數組
@property (nonatomic, readonly) double redComponent;
@property (nonatomic, readonly) double greenComponent;
@property (nonatomic, readonly) double blueComponent;
@property (nonatomic, strong, readonly) UIColor *color;
複製代碼
有了這些之後,咱們就能夠建立這個類的接口了:安全
@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 色彩空間的算法。寫成方法以後以下所示:數據結構
- (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 框架提供的表示屬性依賴的機制以下:架構
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
複製代碼
更詳細的以下:
+ (NSSet *)keyPathsForValuesAffecting<鍵名>
複製代碼
在咱們的例子中以下:
+ (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];
}
複製代碼
如今咱們完整的表達了屬性之間的依賴關係。請注意,咱們能夠把這些屬性連接起來。打個比方,若是咱們寫一個子類去 override redComponent
方法,這些依賴關係仍然能正常工做。
如今讓咱們目光轉向控制器。 NSViewController
的子類擁有 LabColor
model 對象做爲其屬性。
@interface ViewController ()
@property (nonatomic, strong) LabColor *labColor;
@end
複製代碼
咱們把視圖控制器註冊爲觀察者來接收 KVO 的通知,這能夠用如下 NSObject
的方法來實現:
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
複製代碼
這會讓如下方法:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
複製代碼
在當 keyPath
的值改變的時候在觀察者 anObserver
上面被調用。這個 API 看起來有一點嚇人。更糟糕的是,咱們還得記得調用如下的方法
- (void)removeObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
複製代碼
來移除觀察者,不然咱們咱們的 app 會由於某些奇怪的緣由崩潰。
對於大多數的應用來講,KVO 能夠經過輔助類用一種更簡單優雅的方式實現。咱們在視圖控制器添加如下的*觀察記號(Observation token)*屬性:
@property (nonatomic, strong) id colorObserveToken;
複製代碼
當 labColor
在視圖控制器中被設置時,咱們只要 override labColor
的 setter 方法就好了:
- (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 的滑塊控制作出反應:
- (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 示例代碼 中找到。
咱們剛纔所作的事情有點神奇,可是實際上發生的事情是,當 LabColor
實例的 -setLComponent:
等方法被調用的時候如下方法:
- (void)willChangeValueForKey:(NSString *)key
複製代碼
和:
- (void)didChangeValueForKey:(NSString *)key
複製代碼
會在運行 -setLComponent:
中的代碼以前以及以後被自動調用。若是咱們寫了 -setLComponent:
或者咱們選擇使用自動 synthesize 的 lComponent
的 accessor 到時候就會發生這樣的事情。
有些狀況下當咱們須要 override -setLComponent:
而且咱們要控制是否發送鍵值改變的通知的時候,咱們要作如下的事情:
+ (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 方法的話就能夠了,這樣代碼會簡潔不少。
有時咱們會有理由不想用 KeyValueObserver
輔助類。建立另外一個對象會有額外的性能開銷。若是咱們觀察不少個鍵的話,這個開銷可能會變得明顯。
若是咱們在實現一個類的時候把它本身註冊爲觀察者的話:
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
複製代碼
一個很是重要的點是咱們要傳入一個這個類惟一的 context
。咱們推薦把如下代碼
static int const PrivateKVOContext;
複製代碼
寫在這個類 .m
文件的頂端,而後咱們像這樣調用 API 並傳入 PrivateKVOContext
的指針:
[otherObject addObserver:self forKeyPath:@"someKey" options:someOptions context:&PrivateKVOContext];
複製代碼
而後咱們這樣寫 -observeValueForKeyPath:...
的方法:
- (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 的奇怪行爲。
咱們經常須要當一個值改變的時候更新 UI,可是咱們也要在第一次運行代碼的時候更新一次 UI。咱們能夠用 KVO 並添加 NSKeyValueObservingOptionInitial
的選項 來一舉兩得地作好這樣的事情。這將會讓 KVO 通知在調用 -addObserver:forKeyPath:...
到時候也被觸發。
當咱們註冊 KVO 通知的時候,咱們能夠添加 NSKeyValueObservingOptionPrior
選項,這能使咱們在鍵值改變以前被通知。這和-willChangeValueForKey:
被觸發的時間相對應。
若是咱們註冊通知的時候附加了 NSKeyValueObservingOptionPrior
選項,咱們將會收到兩個通知:一個在值變動前,另外一個在變動以後。變動前的通知將會在 change
字典中有不一樣的鍵。咱們能夠像如下這樣區分通知是在改變以前仍是以後被觸發的:
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
// 改變以前
} else {
// 改變以後
}
複製代碼
若是咱們須要改變先後的值,咱們能夠在 KVO 選項中加入 NSKeyValueObservingOptionNew
和/或 NSKeyValueObservingOptionOld
。
更簡單的辦法是用 NSKeyValueObservingOptionPrior
選項,隨後咱們就能夠用如下方式提取出改變先後的值:
id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];
複製代碼
一般來講 KVO 會在 -willChangeValueForKey:
和 -didChangeValueForKey:
被調用的時候存儲相應鍵的值。
KVO 對一些集合類也有很強的支持,如下方法會返回集合對象:
-mutableArrayValueForKey:
-mutableSetValueForKey:
-mutableOrderedSetValueForKey:
複製代碼
咱們將會詳細解釋這是怎麼工做的。若是你使用這些方法,change 字典裏會包含鍵值變化的類型(添加、刪除和替換)。對於有序的集合,change 字典會包含受影響的 index。
集合代理對象和變化的通知在用於更新UI的時候很是有效,尤爲是處理大集合的時候。可是它們須要花費你一些心思。
一個須要注意的地方是,KVO 行爲是同步的,而且發生與所觀察的值發生變化的一樣的線程上。沒有隊列或者 Run-loop 的處理。手動或者自動調用 -didChange...
會觸發 KVO 通知。
因此,當咱們試圖從其餘線程改變屬性值的時候咱們應當十分當心,除非能肯定全部的觀察者都用線程安全的方法處理 KVO 通知。一般來講,咱們不推薦把 KVO 和多線程混起來。若是咱們要用多個隊列和線程,咱們不該該在它們互相之間用 KVO。
KVO 是同步運行的這個特性很是強大,只要咱們在單一線程上面運行(好比主隊列 main queue),KVO 會保證下列兩種狀況的發生:
首先,若是咱們調用一個支持 KVO 的 setter 方法,以下所示:
self.exchangeRate = 2.345;
複製代碼
KVO 能保證全部 exchangeRate
的觀察者在 setter 方法返回前被通知到。
其次,若是某個鍵被觀察的時候附上了 NSKeyValueObservingOptionPrior
選項,直到 -observe...
被調用以前, exchangeRate
的 accessor 方法都會返回一樣的值。
最簡單的 KVC 能讓咱們經過如下的形式訪問屬性:
@property (nonatomic, copy) NSString *name;
複製代碼
取值:
NSString *n = [object valueForKey:@"name"]
複製代碼
設定:
[object setValue:@"Daniel" forKey:@"name"]
複製代碼
值得注意的是這個不只能夠訪問做爲對象屬性,並且也能訪問一些標量(例如 int
和 CGFloat
)和 struct(例如 CGRect
)。Foundation 框架會爲咱們自動封裝它們。舉例來講,若是有如下屬性:
@property (nonatomic) CGFloat height;
複製代碼
咱們能夠這樣設置它:
[object setValue:@(20) forKey:@"height"]
複製代碼
KVC 容許咱們用屬性的字符串名稱來訪問屬性,字符串在這兒叫作鍵。有些狀況下,這會使咱們很是靈活地簡化代碼。咱們下一節介紹例子簡化列表 UI。
KVC 還有更多能夠談的。集合(NSArray
,NSSet
等)結合 KVC 能夠擁有一些強大的集合操做。還有,對象能夠支持用 KVC 經過代理對象訪問很是規的屬性。
假設咱們有這樣一個對象:
@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
屬性:
@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 裏咱們用到的全部鍵的方法,一個把鍵映射到對應的文本框的方法:
- (NSArray *)contactStringKeys;
{
return @[@"name", @"nickname", @"email", @"city"];
}
- (UITextField *)textFieldForModelKey:(NSString *)key;
{
return [self valueForKey:[key stringByAppendingString:@"Field"]];
}
複製代碼
有了這個,咱們能夠從 model 裏更新文本框,以下所示:
- (void)updateTextFields;
{
for (NSString *key in self.contactStringKeys) {
[self textFieldForModelKey:key].text = [self.contact valueForKey:key];
}
}
複製代碼
咱們也能夠用一個 action 方法讓四個文本框都能實時更新 model:
- (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;
}
}
}
複製代碼
注意:咱們以後會添加驗證輸入的部分,在鍵值驗證裏會提到。
最後,咱們須要確認文本框在須要的時候被更新:
- (void)viewWillAppear:(BOOL)animated;
{
[super viewWillAppear:animated];
[self updateTextFields];
}
- (void)setContact:(Contact *)contact
{
_contact = contact;
[self updateTextFields];
}
複製代碼
有了這個,咱們的 detail 視圖控制器 就能正常工做了。
整個項目能夠在 GitHub 上找到。它也用了咱們後面提到的鍵值驗證。
KVC 一樣容許咱們經過關係來訪問對象。假設 person
對象有屬性 address
,address
有屬性 city
,咱們能夠這樣經過 person
來訪問 city
:
[person valueForKeyPath:@"address.city"]
複製代碼
值得注意的是這裏咱們調用 -valueForKeyPath:
而不是 -valueForKey:
。
@property
@property
的 KVC咱們能夠實現一個支持 KVC 而不用 @property
和 @synthesize
或是自動 synthesize 的屬性。最直接的方式是添加 -<key>
和 -set<Key>:
方法。例如咱們想要 name
,咱們這樣作:
- (NSString *)name;
- (void)setName:(NSString *)name;
複製代碼
這徹底等於 @property
的實現方式。
可是當標量和 struct 的值被傳入 nil
的時候尤爲須要注意。假設咱們要 height
屬性支持 KVC 咱們寫了如下的方法:
- (CGFloat)height;
- (void)setHeight:(CGFloat)height;
複製代碼
而後咱們這樣調用:
[object setValue:nil forKey:@"height"]
複製代碼
這會拋出一個 exception。要正確的處理 nil
,咱們要像這樣 override -setNilValueForKey:
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"height"]) {
[self setValue:@0 forKey:key];
} else
[super setNilValueForKey:key];
}
複製代碼
咱們能夠經過 override 這些方法來讓一個類支持 KVC:
- (id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
複製代碼
這也許看起來很怪,但這可讓一個類動態的支持一些鍵的訪問。可是這兩個方法會在性能上拖後腿。
附註:Foundation 框架支持直接訪問實例變量。請當心的使用這個特性。你能夠去查看 +accessInstanceVariablesDirectly
的文檔。這個值默認是 YES
的時候,Foundation 會按照 _<key>
, _is<Key>
, <key>
和 is<Key>
的順序查找實例變量。
一個經常被忽視的 KVC 特性是它對集合操做的支持。舉個例子,咱們能夠這樣來得到一個數組中最大的值:
NSArray *a = @[@4, @84, @2];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);
複製代碼
或者說,咱們有一個 Transaction
對象的數組,對象有屬性 amount
的話,咱們能夠這樣得到最大的 amount
:
NSArray *a = @[transaction1, transaction2, transaction3];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);
複製代碼
當咱們調用 [a valueForKeyPath:@"@max.amount"]
的時候,它會在數組 a
的每一個元素中調用 -valueForKey:@"amount"
而後返回最大的那個。
KVC 的蘋果官方文檔有一個章節 Collection Operators 詳細的講述了相似的用法。
雖然咱們能夠像對待通常的對象同樣用 KVC 深刻集合內部(NSArray
和 NSSet
等),可是經過集合代理對象, KVC 也讓咱們實現一個兼容 KVC 的集合。這是一個頗爲高端的技巧。
當咱們在對象上調用 -valueForKey:
的時候,它能夠返回 NSArray
,NSSet
或是 NSOrderedSet
的集合代理對象。這個類沒有實現一般的 -<Key>
方法,可是它實現了代理對象所須要使用的不少方法。
若是咱們但願一個類支持經過代理對象的 contacts
鍵返回一個 NSArray
,咱們能夠這樣寫:
- (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: |
可選 的一些方法能夠加強代理對象的性能。
雖然只有特殊狀況下咱們用這些代理對象纔會有意義,可是在這些狀況下代理對象很是的有用。想象一下咱們有一個很大的數據結構,調用者不須要(一次性)訪問全部的對象。
舉一個(也許比較作做的)例子說,咱們想寫一個包含有很長一串質數的類。以下所示:
@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
複製代碼
咱們將會運行如下代碼:
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
)中用集合代理。
訪問這些可變的集合有一點點不一樣。調用者在這兒須要調用如下其中一個方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
複製代碼
一個竅門:咱們可讓一個類用如下方法返回可變集合的代理:
- (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 默認自動發出十分詳盡的變化通知。若是咱們要手動實現發送詳細通知的話,咱們得實現這些:
-willChange:valuesAtIndexes:forKey:
-didChange:valuesAtIndexes:forKey:
複製代碼
或者這些:
-willChangeValueForKey:withSetMutation:usingObjects:
-didChangeValueForKey:withSetMutation:usingObjects:
複製代碼
咱們要保證先把自動通知關閉,不然每次改變 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
不是永遠都生效的。並且這不是一個好的設計。
你能夠在 lldb
裏查看一個被觀察對象的全部觀察信息。
(lldb) po [observedObject observationInfo]
複製代碼
這會打印出有關誰觀察誰之類的不少信息。
這個信息的格式不是公開的,咱們不能讓任何東西依賴它,由於蘋果隨時均可以改變它。不過這是一個很強大的排錯工具。
最後提示,KVV 也是 KVC API 的一部分。這是一個用來驗證屬性值的 API,只是它光靠本身很難提供邏輯和功能。
若是咱們寫可以驗證值的 model 類的話,咱們就應該實現 KVV 的 API 來保證一致性。用 KVV 驗證 model 類的值是 Cocoa 的慣例。
讓咱們在一次強調一下:KVC 不會作任何的驗證,也不會調用任何 KVV 的方法。那是你的控制器須要作的事情。經過 KVV 實現你本身的驗證方法會保證它們的一致性。
如下是一個簡單的例子:
- (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:
- (BOOL)validateName:(NSString **)nameP error:(NSError * __autoreleasing *)error
{
if (*nameP == nil) {
*nameP = @"";
return YES;
} else {
*nameP = [*nameP stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
return YES;
}
}
複製代碼
通信錄示例 裏的 DetailViewController
和 Contact
類詳解了這個用法。