鍍金池/ 教程/ iOS/ KVC 和 KVO
與四軸無(wú)人機(jī)的通訊
在沙盒中編寫(xiě)腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開(kāi)發(fā)
Collection View 動(dòng)畫(huà)
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫(huà)
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫(huà)解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫(huà)
常見(jiàn)的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無(wú)人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無(wú)止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫(huà)
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺(jué)
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開(kāi)發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過(guò)程

KVC 和 KVO

Key-value coding (KVC) 和 key-value observing (KVO) 是兩種能讓我們駕馭 Objective-C 動(dòng)態(tài)特性并簡(jiǎn)化代碼的機(jī)制。在這篇文章里,我們將接觸一些如何利用這些特性的例子。

觀察 model 對(duì)象的變化

在 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 屬性依賴于 lComponentaComponent。不論何時(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 示例代碼 中找到。

手動(dòng)通知 vs 自動(dòng)通知

我們剛才所做的事情有點(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)潔很多。

KVO 和 context

有時(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 的奇怪行為。

進(jìn)階 KVO

我們常常需要當(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)你一些心思。

KVO 和線程

一個(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ì)返回同樣的值。

KVC

最簡(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)量(例如 intCGFloat)和 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ī)的屬性。

簡(jiǎn)化列表 UI

假設(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)證

鍵路徑(Key Path)

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:。

Key-Value Coding Without @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ì)的講述了類似的用法。

通過(guò)集合代理對(duì)象來(lái)實(shí)現(xiàn) KVC

雖然我們可以像對(duì)待一般的對(duì)象一樣用 KVC 深入集合內(nèi)部(NSArrayNSSet 等),但是通過(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ì)于 NSSetNSOrderedSet,如果要做同樣的事情,我們需要實(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è)方法。

可變的集合

我們也可以在可變集合(例如 NSMutableArrayNSMutableSet,和 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ā)出兩次通知。

常見(jiàn)的 KVO 錯(cuò)誤

首先,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ì)。

調(diào)試 KVO

你可以在 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ò)工具。

鍵值驗(yàn)證 (KVV)

最后提示,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;
    }
}

通訊錄示例 里的 DetailViewControllerContact 類詳解了這個(gè)用法。