Key-value coding (KVC) 和 key-value observing (KVO) 是兩種能讓我們駕馭 Objective-C 動(dòng)態(tài)特性并簡(jiǎn)化代碼的機(jī)制。在這篇文章里,我們將接觸一些如何利用這些特性的例子。
在 Cocoa 的模型-視圖-控制器 (Model-view-controller)架構(gòu)里,控制器負(fù)責(zé)讓視圖和模型同步。這一共有兩步:當(dāng) model 對(duì)象改變的時(shí)候,視圖應(yīng)該隨之改變以反映模型的變化;當(dāng)用戶和控制器交互的時(shí)候,模型也應(yīng)該做出相應(yīng)的改變。
KVO 能幫助我們讓視圖和模型保持同步??刂破骺梢杂^察視圖依賴的屬性變化。
讓我們看一個(gè)例子:我們的模型類 LabColor
代表一種 Lab色彩空間里的顏色。和 RGB 不同,這種色彩空間有三個(gè)元素 L, a, b。我們要做一個(gè)用來(lái)改變這些值的滑塊和一個(gè)顯示顏色的方塊區(qū)域。
我們的模型類有以下三個(gè)用來(lái)代表顏色的屬性:
@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;
我們需要從這個(gè)類創(chuàng)建一個(gè) UIColor
對(duì)象來(lái)顯示出顏色。我們添加三個(gè)額外的屬性,分別對(duì)應(yīng) R, G, B:
@property (nonatomic, readonly) double redComponent;
@property (nonatomic, readonly) double greenComponent;
@property (nonatomic, readonly) double blueComponent;
@property (nonatomic, strong, readonly) UIColor *color;
有了這些以后,我們就可以創(chuàng)建這個(gè)類的接口了:
@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
維基百科提供了轉(zhuǎn)換 RGB 到 Lab 色彩空間的算法。寫(xiě)成方法之后如下所示:
- (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.];
}
這些代碼沒(méi)什么令人激動(dòng)的地方。有趣的是 greenComponent
屬性依賴于 lComponent
和 aComponent
。不論何時(shí)設(shè)置 lComponent
的值,我們需要讓 RGB 三個(gè) component 中與其相關(guān)的成員以及 color
屬性都要得到通知以保持一致。這一點(diǎn)這在 KVO 中很重要。
Foundation 框架提供的表示屬性依賴的機(jī)制如下:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
更詳細(xì)的如下:
+ (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];
}
現(xiàn)在我們完整的表達(dá)了屬性之間的依賴關(guān)系。請(qǐng)注意,我們可以把這些屬性鏈接起來(lái)。打個(gè)比方,如果我們寫(xiě)一個(gè)子類去 override redComponent
方法,這些依賴關(guān)系仍然能正常工作。
現(xiàn)在讓我們目光轉(zhuǎn)向控制器。 NSViewController
的子類擁有 LabColor
model 對(duì)象作為其屬性。
@interface ViewController ()
@property (nonatomic, strong) LabColor *labColor;
@end
我們把視圖控制器注冊(cè)為觀察者來(lái)接收 KVO 的通知,這可以用以下 NSObject
的方法來(lái)實(shí)現(xiàn):
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
這會(huì)讓以下方法:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
在當(dāng) keyPath
的值改變的時(shí)候在觀察者 anObserver
上面被調(diào)用。這個(gè) API 看起來(lái)有一點(diǎn)嚇人。更糟糕的是,我們還得記得調(diào)用以下的方法
- (void)removeObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
來(lái)移除觀察者,否則我們我們的 app 會(huì)因?yàn)槟承┢婀值脑虮罎ⅰ?/p>
對(duì)于大多數(shù)的應(yīng)用來(lái)說(shuō),KVO 可以通過(guò)輔助類用一種更簡(jiǎn)單優(yōu)雅的方式實(shí)現(xiàn)。我們?cè)谝晥D控制器添加以下的觀察記號(hào)(Observation token)屬性:
@property (nonatomic, strong) id colorObserveToken;
當(dāng) labColor
在視圖控制器中被設(shè)置時(shí),我們只要 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:
的調(diào)用,讓視圖控制器遠(yuǎn)離雜亂的代碼。
視圖控制器需要對(duì) L,a,b 的滑塊控制做出反應(yīng):
- (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 示例代碼 中找到。
我們剛才所做的事情有點(diǎn)神奇,但是實(shí)際上發(fā)生的事情是,當(dāng) LabColor
實(shí)例的 -setLComponent:
等方法被調(diào)用的時(shí)候以下方法:
- (void)willChangeValueForKey:(NSString *)key
和:
- (void)didChangeValueForKey:(NSString *)key
會(huì)在運(yùn)行 -setLComponent:
中的代碼之前以及之后被自動(dòng)調(diào)用。如果我們寫(xiě)了 -setLComponent:
或者我們選擇使用自動(dòng) synthesize 的 lComponent
的 accessor 到時(shí)候就會(huì)發(fā)生這樣的事情。
有些情況下當(dāng)我們需要 override -setLComponent:
并且我們要控制是否發(fā)送鍵值改變的通知的時(shí)候,我們要做以下的事情:
+ (BOOL)automaticallyNotifiesObserversForLComponent;
{
return NO;
}
- (void)setLComponent:(double)lComponent;
{
if (_lComponent == lComponent) {
return;
}
[self willChangeValueForKey:@"lComponent"];
_lComponent = lComponent;
[self didChangeValueForKey:@"lComponent"];
}
我們關(guān)閉了 -willChangeValueForKey:
和 -didChangeValueForKey:
的自動(dòng)調(diào)用,然后我們手動(dòng)調(diào)用他們。我們只應(yīng)該在關(guān)閉了自動(dòng)調(diào)用的時(shí)候我們才需要在 setter 方法里手動(dòng)調(diào)用 -willChangeValueForKey:
和 -didChangeValueForKey:
。大多數(shù)情況下,這樣優(yōu)化不會(huì)給我們帶來(lái)太多好處。
如果我們?cè)?accessor 方法之外改變實(shí)例對(duì)象(如 _lComponent
),我們要特別小心地和剛才一樣封裝 -willChangeValueForKey:
和 -didChangeValueForKey:
。不過(guò)在多數(shù)情況下,我們只用 accessor 方法的話就可以了,這樣代碼會(huì)簡(jiǎn)潔很多。
有時(shí)我們會(huì)有理由不想用 KeyValueObserver
輔助類。創(chuàng)建另一個(gè)對(duì)象會(huì)有額外的性能開(kāi)銷。如果我們觀察很多個(gè)鍵的話,這個(gè)開(kāi)銷可能會(huì)變得明顯。
如果我們?cè)趯?shí)現(xiàn)一個(gè)類的時(shí)候把它自己注冊(cè)為觀察者的話:
- (void)addObserver:(NSObject *)anObserver
forKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
context:(void *)context
一個(gè)非常重要的點(diǎn)是我們要傳入一個(gè)這個(gè)類唯一的 context
。我們推薦把以下代碼
static int const PrivateKVOContext;
寫(xiě)在這個(gè)類 .m
文件的頂端,然后我們像這樣調(diào)用 API 并傳入 PrivateKVOContext
的指針:
[otherObject addObserver:self forKeyPath:@"someKey" options:someOptions context:&PrivateKVOContext];
然后我們這樣寫(xiě) -observeValueForKeyPath:...
的方法:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (context == &PrivateKVOContext) {
// 這里寫(xiě)相關(guān)的觀察代碼
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
這將確保我們寫(xiě)的子類都是正確的。如此一來(lái),子類和父類都能安全的觀察同樣的鍵值而不會(huì)沖突。否則我們將會(huì)碰到難以 debug 的奇怪行為。
我們常常需要當(dāng)一個(gè)值改變的時(shí)候更新 UI,但是我們也要在第一次運(yùn)行代碼的時(shí)候更新一次 UI。我們可以用 KVO 并添加 NSKeyValueObservingOptionInitial
的選項(xiàng) 來(lái)一箭雙雕地做好這樣的事情。這將會(huì)讓 KVO 通知在調(diào)用 -addObserver:forKeyPath:...
到時(shí)候也被觸發(fā)。
當(dāng)我們注冊(cè) KVO 通知的時(shí)候,我們可以添加 NSKeyValueObservingOptionPrior
選項(xiàng),這能使我們?cè)阪I值改變之前被通知。這和-willChangeValueForKey:
被觸發(fā)的時(shí)間相對(duì)應(yīng)。
如果我們注冊(cè)通知的時(shí)候附加了 NSKeyValueObservingOptionPrior
選項(xiàng),我們將會(huì)收到兩個(gè)通知:一個(gè)在值變更前,另一個(gè)在變更之后。變更前的通知將會(huì)在 change
字典中有不同的鍵。我們可以像以下這樣區(qū)分通知是在改變之前還是之后被觸發(fā)的:
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
// 改變之前
} else {
// 改變之后
}
如果我們需要改變前后的值,我們可以在 KVO 選項(xiàng)中加入 NSKeyValueObservingOptionNew
和/或 NSKeyValueObservingOptionOld
。
更簡(jiǎn)單的辦法是用 NSKeyValueObservingOptionPrior
選項(xiàng),隨后我們就可以用以下方式提取出改變前后的值:
id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];
通常來(lái)說(shuō) KVO 會(huì)在 -willChangeValueForKey:
和 -didChangeValueForKey:
被調(diào)用的時(shí)候存儲(chǔ)相應(yīng)鍵的值。
KVO 對(duì)一些集合類也有很強(qiáng)的支持,以下方法會(huì)返回集合對(duì)象:
-mutableArrayValueForKey:
-mutableSetValueForKey:
-mutableOrderedSetValueForKey:
我們將會(huì)詳細(xì)解釋這是怎么工作的。如果你使用這些方法,change 字典里會(huì)包含鍵值變化的類型(添加、刪除和替換)。對(duì)于有序的集合,change 字典會(huì)包含受影響的 index。
集合代理對(duì)象和變化的通知在用于更新UI的時(shí)候非常有效,尤其是處理大集合的時(shí)候。但是它們需要花費(fèi)你一些心思。
一個(gè)需要注意的地方是,KVO 行為是同步的,并且發(fā)生與所觀察的值發(fā)生變化的同樣的線程上。沒(méi)有隊(duì)列或者 Run-loop 的處理。手動(dòng)或者自動(dòng)調(diào)用 -didChange...
會(huì)觸發(fā) KVO 通知。
所以,當(dāng)我們?cè)噲D從其他線程改變屬性值的時(shí)候我們應(yīng)當(dāng)十分小心,除非能確定所有的觀察者都用線程安全的方法處理 KVO 通知。通常來(lái)說(shuō),我們不推薦把 KVO 和多線程混起來(lái)。如果我們要用多個(gè)隊(duì)列和線程,我們不應(yīng)該在它們互相之間用 KVO。
KVO 是同步運(yùn)行的這個(gè)特性非常強(qiáng)大,只要我們?cè)趩我痪€程上面運(yùn)行(比如主隊(duì)列 main queue),KVO 會(huì)保證下列兩種情況的發(fā)生:
首先,如果我們調(diào)用一個(gè)支持 KVO 的 setter 方法,如下所示:
self.exchangeRate = 2.345;
KVO 能保證所有 exchangeRate
的觀察者在 setter 方法返回前被通知到。
其次,如果某個(gè)鍵被觀察的時(shí)候附上了 NSKeyValueObservingOptionPrior
選項(xiàng),直到 -observe...
被調(diào)用之前, exchangeRate
的 accessor 方法都會(huì)返回同樣的值。
最簡(jiǎn)單的 KVC 能讓我們通過(guò)以下的形式訪問(wèn)屬性:
@property (nonatomic, copy) NSString *name;
取值:
NSString *n = [object valueForKey:@"name"]
設(shè)定:
[object setValue:@"Daniel" forKey:@"name"]
值得注意的是這個(gè)不僅可以訪問(wèn)作為對(duì)象屬性,而且也能訪問(wèn)一些標(biāo)量(例如 int
和 CGFloat
)和 struct(例如 CGRect
)。Foundation 框架會(huì)為我們自動(dòng)封裝它們。舉例來(lái)說(shuō),如果有以下屬性:
@property (nonatomic) CGFloat height;
我們可以這樣設(shè)置它:
[object setValue:@(20) forKey:@"height"]
KVC 允許我們用屬性的字符串名稱來(lái)訪問(wèn)屬性,字符串在這兒叫做鍵。有些情況下,這會(huì)使我們非常靈活地簡(jiǎn)化代碼。我們下一節(jié)介紹例子簡(jiǎn)化列表 UI。
KVC 還有更多可以談的。集合(NSArray
,NSSet
等)結(jié)合 KVC 可以擁有一些強(qiáng)大的集合操作。還有,對(duì)象可以支持用 KVC 通過(guò)代理對(duì)象訪問(wèn)非常規(guī)的屬性。
假設(shè)我們有這樣一個(gè)對(duì)象:
@interface Contact : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickname;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *city;
@end
還有一個(gè) detail 視圖控制器,含有四個(gè)對(duì)應(yīng)的 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
我們可以簡(jiǎn)化更新 UI 的邏輯。首先我們需要兩個(gè)方法:一個(gè)返回 model 里我們用到的所有鍵的方法,一個(gè)把鍵映射到對(duì)應(yīng)的文本框的方法:
- (NSArray *)contactStringKeys;
{
return @[@"name", @"nickname", @"email", @"city"];
}
- (UITextField *)textFieldForModelKey:(NSString *)key;
{
return [self valueForKey:[key stringByAppendingString:@"Field"]];
}
有了這個(gè),我們可以從 model 里更新文本框,如下所示:
- (void)updateTextFields;
{
for (NSString *key in self.contactStringKeys) {
[self textFieldForModelKey:key].text = [self.contact valueForKey:key];
}
}
我們也可以用一個(gè) action 方法讓四個(gè)文本框都能實(shí)時(shí)更新 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;
}
}
}
注意:我們之后會(huì)添加驗(yàn)證輸入的部分,在鍵值驗(yàn)證里會(huì)提到。
最后,我們需要確認(rèn)文本框在需要的時(shí)候被更新:
- (void)viewWillAppear:(BOOL)animated;
{
[super viewWillAppear:animated];
[self updateTextFields];
}
- (void)setContact:(Contact *)contact
{
_contact = contact;
[self updateTextFields];
}
有了這個(gè),我們的 detail 視圖控制器 就能正常工作了。
整個(gè)項(xiàng)目可以在 GitHub 上找到。它也用了我們后面提到的鍵值驗(yàn)證。
KVC 同樣允許我們通過(guò)關(guān)系來(lái)訪問(wèn)對(duì)象。假設(shè) person
對(duì)象有屬性 address
,address
有屬性 city
,我們可以這樣通過(guò) person
來(lái)訪問(wèn) city
:
[person valueForKeyPath:@"address.city"]
值得注意的是這里我們調(diào)用 -valueForKeyPath:
而不是 -valueForKey:
。
@property
@property
的 KVC我們可以實(shí)現(xiàn)一個(gè)支持 KVC 而不用 @property
和 @synthesize
或是自動(dòng) synthesize 的屬性。最直接的方式是添加 -<key>
和 -set<Key>:
方法。例如我們想要 name
,我們這樣做:
- (NSString *)name;
- (void)setName:(NSString *)name;
這完全等于 @property
的實(shí)現(xiàn)方式。
但是當(dāng)標(biāo)量和 struct 的值被傳入 nil
的時(shí)候尤其需要注意。假設(shè)我們要 height
屬性支持 KVC 我們寫(xiě)了以下的方法:
- (CGFloat)height;
- (void)setHeight:(CGFloat)height;
然后我們這樣調(diào)用:
[object setValue:nil forKey:@"height"]
這會(huì)拋出一個(gè) exception。要正確的處理 nil
,我們要像這樣 override -setNilValueForKey:
- (void)setNilValueForKey:(NSString *)key
{
if ([key isEqualToString:@"height"]) {
[self setValue:@0 forKey:key];
} else
[super setNilValueForKey:key];
}
我們可以通過(guò) override 這些方法來(lái)讓一個(gè)類支持 KVC:
- (id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
這也許看起來(lái)很怪,但這可以讓一個(gè)類動(dòng)態(tài)的支持一些鍵的訪問(wèn)。但是這兩個(gè)方法會(huì)在性能上拖后腿。
附注:Foundation 框架支持直接訪問(wèn)實(shí)例變量。請(qǐng)小心的使用這個(gè)特性。你可以去查看 +accessInstanceVariablesDirectly
的文檔。這個(gè)值默認(rèn)是 YES
的時(shí)候,F(xiàn)oundation 會(huì)按照 _<key>
, _is<Key>
, <key>
和 is<Key>
的順序查找實(shí)例變量。
一個(gè)常常被忽視的 KVC 特性是它對(duì)集合操作的支持。舉個(gè)例子,我們可以這樣來(lái)獲得一個(gè)數(shù)組中最大的值:
NSArray *a = @[@4, @84, @2];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);
或者說(shuō),我們有一個(gè) Transaction
對(duì)象的數(shù)組,對(duì)象有屬性 amount
的話,我們可以這樣獲得最大的 amount
:
NSArray *a = @[transaction1, transaction2, transaction3];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);
當(dāng)我們調(diào)用 [a valueForKeyPath:@"@max.amount"]
的時(shí)候,它會(huì)在數(shù)組 a
的每個(gè)元素中調(diào)用 -valueForKey:@"amount"
然后返回最大的那個(gè)。
KVC 的蘋(píng)果官方文檔有一個(gè)章節(jié) Collection Operators 詳細(xì)的講述了類似的用法。
雖然我們可以像對(duì)待一般的對(duì)象一樣用 KVC 深入集合內(nèi)部(NSArray
和 NSSet
等),但是通過(guò)集合代理對(duì)象, KVC 也讓我們實(shí)現(xiàn)一個(gè)兼容 KVC 的集合。這是一個(gè)頗為高端的技巧。
當(dāng)我們?cè)趯?duì)象上調(diào)用 -valueForKey:
的時(shí)候,它可以返回 NSArray
,NSSet
或是 NSOrderedSet
的集合代理對(duì)象。這個(gè)類沒(méi)有實(shí)現(xiàn)通常的 -<Key>
方法,但是它實(shí)現(xiàn)了代理對(duì)象所需要使用的很多方法。
如果我們希望一個(gè)類支持通過(guò)代理對(duì)象的 contacts
鍵返回一個(gè) NSArray
,我們可以這樣寫(xiě):
- (NSUInteger)countOfContacts;
- (id)objectInContactsAtIndex:(NSUInteger)idx;
這樣做的話,當(dāng)我們調(diào)用 [object valueForKey:@"contacts”]
的時(shí)候,它會(huì)返回一個(gè)由這兩個(gè)方法來(lái)代理所有調(diào)用方法的 NSArray
對(duì)象。這個(gè)數(shù)組支持所有正常的對(duì) NSArray
的調(diào)用。換句話說(shuō),調(diào)用者并不知道返回的是一個(gè)真正的 NSArray
, 還是一個(gè)代理的數(shù)組。
對(duì)于 NSSet
和 NSOrderedSet
,如果要做同樣的事情,我們需要實(shí)現(xiàn)的方法是:
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: |
||
可選(增強(qiáng)性能) | ||
-get<Key>:range: |
可選(增強(qiáng)性能) | |
-get<Key>:range: |
可選 的一些方法可以增強(qiáng)代理對(duì)象的性能。
雖然只有特殊情況下我們用這些代理對(duì)象才會(huì)有意義,但是在這些情況下代理對(duì)象非常的有用。想象一下我們有一個(gè)很大的數(shù)據(jù)結(jié)構(gòu),調(diào)用者不需要(一次性)訪問(wèn)所有的對(duì)象。
舉一個(gè)(也許比較做作的)例子說(shuō),我們想寫(xiě)一個(gè)包含有很長(zhǎng)一串質(zhì)數(shù)的類。如下所示:
@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
我們將會(huì)運(yùn)行以下代碼:
Primes *primes = [[Primes alloc] init];
NSLog(@"The last prime is %@", [primes.primes lastObject]);
這將會(huì)調(diào)用一次 -countOfPrimes
和一次傳入?yún)?shù) idx
作為最后一個(gè)索引的 -objectInPrimesAtIndex:
。為了只取出最后一個(gè)值,它不需要先把所有的數(shù)封裝成 NSNumber
然后把它們都導(dǎo)入 NSArray
。
在一個(gè)復(fù)雜一點(diǎn)的例子中,通訊錄編輯器示例 app 用同樣的方法把 C++ std::vector
封裝以來(lái)。它詳細(xì)說(shuō)明了應(yīng)該怎么利用這個(gè)方法。
我們也可以在可變集合(例如 NSMutableArray
,NSMutableSet
,和 NSMutableOrderedSet
)中用集合代理。
訪問(wèn)這些可變的集合有一點(diǎn)點(diǎn)不同。調(diào)用者在這兒需要調(diào)用以下其中一個(gè)方法:
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
一個(gè)竅門(mén):我們可以讓一個(gè)類用以下方法返回可變集合的代理:
- (NSMutableArray *)mutableContacts;
{
return [self mutableArrayValueForKey:@"wrappedContacts"];
}
然后在實(shí)現(xiàn)鍵 wrappedContacts
的一些方法。
我們需要實(shí)現(xiàn)上面的不變集合的兩個(gè)方法,還有以下的幾個(gè):
NSMutableArray?/?NSMutableOrderedSet??????? | NSMutableSet????????????????????????????? |
---|---|
至少實(shí)現(xiàn)一個(gè)插入方法和一個(gè)刪除方法 | 至少實(shí)現(xiàn)一個(gè)插入方法和一個(gè)刪除方法 |
-insertObject:in<Key>AtIndex: |
-add<Key>Object: |
-removeObjectFrom<Key>AtIndex: |
-remove<Key>Object: |
-insert<Key>:atIndexes: |
-add<Key>: |
-remove<Key>AtIndexes: |
-remove<Key>: |
可選(增強(qiáng)性能)以下方法二選一 | 可選(增強(qiáng)性能) |
-replaceObjectIn<Key>AtIndex:withObject: |
-intersect<Key>: |
-replace<Key>AtIndexes:with<Key>: |
-set<Key>: |
上面提到,這些可變集合代理對(duì)象和 KVO 結(jié)合起來(lái)也十分強(qiáng)大。KVO 機(jī)制能在這些集合改變的時(shí)候把詳細(xì)的變化放進(jìn) change 字典中。
有批量更新(需要傳入多個(gè)對(duì)象)的方法,也有只改變一個(gè)對(duì)象的方法。我們推薦選擇相對(duì)于給定任務(wù)來(lái)說(shuō)最容易實(shí)現(xiàn)的那個(gè)來(lái)寫(xiě),雖然我們有一點(diǎn)點(diǎn)傾向于選擇批量更新的那個(gè)。
在實(shí)現(xiàn)這些方法的時(shí)候,我們要對(duì)自動(dòng)和手動(dòng)的 KVO 之間的差別十分小心。Foundation 默認(rèn)自動(dòng)發(fā)出十分詳盡的變化通知。如果我們要手動(dòng)實(shí)現(xiàn)發(fā)送詳細(xì)通知的話,我們得實(shí)現(xiàn)這些:
-willChange:valuesAtIndexes:forKey:
-didChange:valuesAtIndexes:forKey:
或者這些:
-willChangeValueForKey:withSetMutation:usingObjects:
-didChangeValueForKey:withSetMutation:usingObjects:
我們要保證先把自動(dòng)通知關(guān)閉,否則每次改變 KVO 都會(huì)發(fā)出兩次通知。
首先,KVO 兼容是 API 的一部分。如果類的所有者不保證某個(gè)屬性兼容 KVO,我們就不能保證 KVO 正常工作。蘋(píng)果文檔里有 KVO 兼容屬性的文檔。例如,NSProgress
類的大多數(shù)屬性都是兼容 KVO 的。
當(dāng)做出改變以后,有些人試著放空的 -willChange
和 -didChange
方法來(lái)強(qiáng)制 KVO 的觸發(fā)。KVO 通知雖然會(huì)生效,但是這樣做破壞了有依賴于 NSKeyValueObservingOld
選項(xiàng)的觀察者。詳細(xì)來(lái)說(shuō),這影響了 KVO 對(duì)觀察鍵路徑 (key path) 的原生支持。KVO 在觀察鍵路徑 (key path) 時(shí)依賴于 NSKeyValueObservingOld
屬性。
我們也要指出有些集合是不能被觀察的。KVO 旨在觀察關(guān)系 (relationship) 而不是集合。我們不能觀察 NSArray
,我們只能觀察一個(gè)對(duì)象的屬性——而這個(gè)屬性有可能是 NSArray
。舉例說(shuō),如果我們有一個(gè) ContactList
對(duì)象,我們可以觀察它的 contacts
屬性。但是我們不能向要觀察對(duì)象的 -addObserver:forKeyPath:...
傳入一個(gè) NSArray
。
相似地,觀察 self
不是永遠(yuǎn)都生效的。而且這不是一個(gè)好的設(shè)計(jì)。
你可以在 lldb
里查看一個(gè)被觀察對(duì)象的所有觀察信息。
(lldb) po [observedObject observationInfo]
這會(huì)打印出有關(guān)誰(shuí)觀察誰(shuí)之類的很多信息。
這個(gè)信息的格式不是公開(kāi)的,我們不能讓任何東西依賴它,因?yàn)樘O(píng)果隨時(shí)都可以改變它。不過(guò)這是一個(gè)很強(qiáng)大的排錯(cuò)工具。
最后提示,KVV 也是 KVC API 的一部分。這是一個(gè)用來(lái)驗(yàn)證屬性值的 API,只是它光靠自己很難提供邏輯和功能。
如果我們寫(xiě)能夠驗(yàn)證值的 model 類的話,我們就應(yīng)該實(shí)現(xiàn) KVV 的 API 來(lái)保證一致性。用 KVV 驗(yàn)證 model 類的值是 Cocoa 的慣例。
讓我們?cè)谝淮螐?qiáng)調(diào)一下:KVC 不會(huì)做任何的驗(yàn)證,也不會(huì)調(diào)用任何 KVV 的方法。那是你的控制器需要做的事情。通過(guò) KVV 實(shí)現(xiàn)你自己的驗(yàn)證方法會(huì)保證它們的一致性。
以下是一個(gè)簡(jiǎn)單的例子:
- (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;
}
它強(qiáng)大之處在于,當(dāng) model 類(Contact
)驗(yàn)證 name
的時(shí)候,會(huì)有機(jī)會(huì)去處理名字。
如果我們想讓名字不要有前后的空白字符,我們應(yīng)該把這些邏輯放在 model 對(duì)象里面。Contact
類可以像這樣實(shí)現(xiàn) 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
類詳解了這個(gè)用法。