在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

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

KVC 和 KVO

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

觀察 model 對象的變化

在 Cocoa 的模型-視圖-控制器 (Model-view-controller)架構(gòu)里,控制器負(fù)責(zé)讓視圖和模型同步。這一共有兩步:當(dāng) model 對象改變的時(shí)候,視圖應(yīng)該隨之改變以反映模型的變化;當(dāng)用戶和控制器交互的時(shí)候,模型也應(yīng)該做出相應(yīng)的改變。

KVO 能幫助我們讓視圖和模型保持同步。控制器可以觀察視圖依賴的屬性變化。

讓我們看一個(gè)例子:我們的模型類 LabColor 代表一種 Lab色彩空間里的顏色。和 RGB 不同,這種色彩空間有三個(gè)元素 L, a, b。我們要做一個(gè)用來改變這些值的滑塊和一個(gè)顯示顏色的方塊區(qū)域。

我們的模型類有以下三個(gè)用來代表顏色的屬性:

@property (nonatomic) double lComponent;
@property (nonatomic) double aComponent;
@property (nonatomic) double bComponent;

依賴的屬性

我們需要從這個(gè)類創(chuàng)建一個(gè) UIColor 對象來顯示出顏色。我們添加三個(gè)額外的屬性,分別對應(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 色彩空間的算法。寫成方法之后如下所示:

- (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 屬性依賴于 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)系。請注意,我們可以把這些屬性鏈接起來。打個(gè)比方,如果我們寫一個(gè)子類去 override redComponent 方法,這些依賴關(guān)系仍然能正常工作。

觀察變化

現(xiàn)在讓我們目光轉(zhuǎn)向控制器。 NSViewController 的子類擁有 LabColor model 對象作為其屬性。

@interface ViewController ()

@property (nonatomic, strong) LabColor *labColor;

@end

我們把視圖控制器注冊為觀察者來接收 KVO 的通知,這可以用以下 NSObject 的方法來實(shí)現(xiàn):

- (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

在當(dāng) keyPath 的值改變的時(shí)候在觀察者 anObserver 上面被調(diào)用。這個(gè) API 看起來有一點(diǎn)嚇人。更糟糕的是,我們還得記得調(diào)用以下的方法

- (void)removeObserver:(NSObject *)anObserver
            forKeyPath:(NSString *)keyPath

來移除觀察者,否則我們我們的 app 會因?yàn)槟承┢婀值脑虮罎ⅰ?/p>

對于大多數(shù)的應(yīng)用來說,KVO 可以通過輔助類用一種更簡單優(yōu)雅的方式實(shí)現(xiàn)。我們在視圖控制器添加以下的觀察記號(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)離雜亂的代碼。

整合到一起

視圖控制器需要對 L,ab 的滑塊控制做出反應(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 示例代碼 中找到。

手動通知 vs 自動通知

我們剛才所做的事情有點(diǎn)神奇,但是實(shí)際上發(fā)生的事情是,當(dāng) LabColor 實(shí)例的 -setLComponent: 等方法被調(diào)用的時(shí)候以下方法:

- (void)willChangeValueForKey:(NSString *)key

和:

- (void)didChangeValueForKey:(NSString *)key

會在運(yùn)行 -setLComponent: 中的代碼之前以及之后被自動調(diào)用。如果我們寫了 -setLComponent: 或者我們選擇使用自動 synthesize 的 lComponent 的 accessor 到時(shí)候就會發(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: 的自動調(diào)用,然后我們手動調(diào)用他們。我們只應(yīng)該在關(guān)閉了自動調(diào)用的時(shí)候我們才需要在 setter 方法里手動調(diào)用 -willChangeValueForKey:-didChangeValueForKey:。大多數(shù)情況下,這樣優(yōu)化不會給我們帶來太多好處。

如果我們在 accessor 方法之外改變實(shí)例對象(如 _lComponent ),我們要特別小心地和剛才一樣封裝 -willChangeValueForKey:-didChangeValueForKey:。不過在多數(shù)情況下,我們只用 accessor 方法的話就可以了,這樣代碼會簡潔很多。

KVO 和 context

有時(shí)我們會有理由不想用 KeyValueObserver 輔助類。創(chuàng)建另一個(gè)對象會有額外的性能開銷。如果我們觀察很多個(gè)鍵的話,這個(gè)開銷可能會變得明顯。

如果我們在實(shí)現(xiàn)一個(gè)類的時(shí)候把它自己注冊為觀察者的話:

- (void)addObserver:(NSObject *)anObserver
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context

一個(gè)非常重要的點(diǎn)是我們要傳入一個(gè)這個(gè)類唯一的 context。我們推薦把以下代碼

static int const PrivateKVOContext;

寫在這個(gè)類 .m 文件的頂端,然后我們像這樣調(diào)用 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) {
        // 這里寫相關(guān)的觀察代碼
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

這將確保我們寫的子類都是正確的。如此一來,子類和父類都能安全的觀察同樣的鍵值而不會沖突。否則我們將會碰到難以 debug 的奇怪行為。

進(jìn)階 KVO

我們常常需要當(dāng)一個(gè)值改變的時(shí)候更新 UI,但是我們也要在第一次運(yùn)行代碼的時(shí)候更新一次 UI。我們可以用 KVO 并添加 NSKeyValueObservingOptionInitial 的選項(xiàng) 來一箭雙雕地做好這樣的事情。這將會讓 KVO 通知在調(diào)用 -addObserver:forKeyPath:... 到時(shí)候也被觸發(fā)。

之前和之后

當(dāng)我們注冊 KVO 通知的時(shí)候,我們可以添加 NSKeyValueObservingOptionPrior 選項(xiàng),這能使我們在鍵值改變之前被通知。這和-willChangeValueForKey:被觸發(fā)的時(shí)間相對應(yīng)。

如果我們注冊通知的時(shí)候附加了 NSKeyValueObservingOptionPrior 選項(xiàng),我們將會收到兩個(gè)通知:一個(gè)在值變更前,另一個(gè)在變更之后。變更前的通知將會在 change 字典中有不同的鍵。我們可以像以下這樣區(qū)分通知是在改變之前還是之后被觸發(fā)的:

if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
    // 改變之前
} else {
    // 改變之后
}

如果我們需要改變前后的值,我們可以在 KVO 選項(xiàng)中加入 NSKeyValueObservingOptionNew 和/或 NSKeyValueObservingOptionOld。

更簡單的辦法是用 NSKeyValueObservingOptionPrior 選項(xiàng),隨后我們就可以用以下方式提取出改變前后的值:

id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];

通常來說 KVO 會在 -willChangeValueForKey:-didChangeValueForKey: 被調(diào)用的時(shí)候存儲相應(yīng)鍵的值。

索引

KVO 對一些集合類也有很強(qiáng)的支持,以下方法會返回集合對象:

-mutableArrayValueForKey:
-mutableSetValueForKey:
-mutableOrderedSetValueForKey:

我們將會詳細(xì)解釋這是怎么工作的。如果你使用這些方法,change 字典里會包含鍵值變化的類型(添加、刪除和替換)。對于有序的集合,change 字典會包含受影響的 index。

集合代理對象和變化的通知在用于更新UI的時(shí)候非常有效,尤其是處理大集合的時(shí)候。但是它們需要花費(fèi)你一些心思。

KVO 和線程

一個(gè)需要注意的地方是,KVO 行為是同步的,并且發(fā)生與所觀察的值發(fā)生變化的同樣的線程上。沒有隊(duì)列或者 Run-loop 的處理。手動或者自動調(diào)用 -didChange... 會觸發(fā) KVO 通知。

所以,當(dāng)我們試圖從其他線程改變屬性值的時(shí)候我們應(yīng)當(dāng)十分小心,除非能確定所有的觀察者都用線程安全的方法處理 KVO 通知。通常來說,我們不推薦把 KVO 和多線程混起來。如果我們要用多個(gè)隊(duì)列和線程,我們不應(yīng)該在它們互相之間用 KVO。

KVO 是同步運(yùn)行的這個(gè)特性非常強(qiáng)大,只要我們在單一線程上面運(yùn)行(比如主隊(duì)列 main queue),KVO 會保證下列兩種情況的發(fā)生:

首先,如果我們調(diào)用一個(gè)支持 KVO 的 setter 方法,如下所示:

self.exchangeRate = 2.345;

KVO 能保證所有 exchangeRate 的觀察者在 setter 方法返回前被通知到。

其次,如果某個(gè)鍵被觀察的時(shí)候附上了 NSKeyValueObservingOptionPrior 選項(xiàng),直到 -observe... 被調(diào)用之前, exchangeRate 的 accessor 方法都會返回同樣的值。

KVC

最簡單的 KVC 能讓我們通過以下的形式訪問屬性:

@property (nonatomic, copy) NSString *name;

取值:

NSString *n = [object valueForKey:@"name"]

設(shè)定:

[object setValue:@"Daniel" forKey:@"name"]

值得注意的是這個(gè)不僅可以訪問作為對象屬性,而且也能訪問一些標(biāo)量(例如 intCGFloat)和 struct(例如 CGRect)。Foundation 框架會為我們自動封裝它們。舉例來說,如果有以下屬性:

@property (nonatomic) CGFloat height;

我們可以這樣設(shè)置它:

[object setValue:@(20) forKey:@"height"]

KVC 允許我們用屬性的字符串名稱來訪問屬性,字符串在這兒叫做。有些情況下,這會使我們非常靈活地簡化代碼。我們下一節(jié)介紹例子簡化列表 UI。

KVC 還有更多可以談的。集合(NSArray,NSSet 等)結(jié)合 KVC 可以擁有一些強(qiáng)大的集合操作。還有,對象可以支持用 KVC 通過代理對象訪問非常規(guī)的屬性。

簡化列表 UI

假設(shè)我們有這樣一個(gè)對象:

@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è)對應(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

我們可以簡化更新 UI 的邏輯。首先我們需要兩個(gè)方法:一個(gè)返回 model 里我們用到的所有鍵的方法,一個(gè)把鍵映射到對應(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;
        }
    }
}

注意:我們之后會添加驗(yàn)證輸入的部分,在鍵值驗(yàn)證里會提到。

最后,我們需要確認(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ān)系來訪問對象。假設(shè) person 對象有屬性 address,address 有屬性 city,我們可以這樣通過 person 來訪問 city

[person valueForKeyPath:@"address.city"]

值得注意的是這里我們調(diào)用 -valueForKeyPath: 而不是 -valueForKey:。

Key-Value Coding Without @property

不需要 @property 的 KVC

我們可以實(shí)現(xiàn)一個(gè)支持 KVC 而不用 @property@synthesize 或是自動 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 我們寫了以下的方法:

- (CGFloat)height;
- (void)setHeight:(CGFloat)height;

然后我們這樣調(diào)用:

[object setValue:nil forKey:@"height"]

這會拋出一個(gè) exception。要正確的處理 nil,我們要像這樣 override -setNilValueForKey:

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"height"]) {
        [self setValue:@0 forKey:key];
    } else
        [super setNilValueForKey:key];
}

我們可以通過 override 這些方法來讓一個(gè)類支持 KVC:

- (id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;

這也許看起來很怪,但這可以讓一個(gè)類動態(tài)的支持一些鍵的訪問。但是這兩個(gè)方法會在性能上拖后腿。

附注:Foundation 框架支持直接訪問實(shí)例變量。請小心的使用這個(gè)特性。你可以去查看 +accessInstanceVariablesDirectly 的文檔。這個(gè)值默認(rèn)是 YES 的時(shí)候,F(xiàn)oundation 會按照 _<key>, _is<Key>, <key>is<Key> 的順序查找實(shí)例變量。

集合的操作

一個(gè)常常被忽視的 KVC 特性是它對集合操作的支持。舉個(gè)例子,我們可以這樣來獲得一個(gè)數(shù)組中最大的值:

NSArray *a = @[@4, @84, @2];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);

或者說,我們有一個(gè) Transaction 對象的數(shù)組,對象有屬性 amount 的話,我們可以這樣獲得最大的 amount

NSArray *a = @[transaction1, transaction2, transaction3];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);

當(dāng)我們調(diào)用 [a valueForKeyPath:@"@max.amount"] 的時(shí)候,它會在數(shù)組 a 的每個(gè)元素中調(diào)用 -valueForKey:@"amount" 然后返回最大的那個(gè)。

KVC 的蘋果官方文檔有一個(gè)章節(jié) Collection Operators 詳細(xì)的講述了類似的用法。

通過集合代理對象來實(shí)現(xiàn) KVC

雖然我們可以像對待一般的對象一樣用 KVC 深入集合內(nèi)部(NSArrayNSSet 等),但是通過集合代理對象, KVC 也讓我們實(shí)現(xiàn)一個(gè)兼容 KVC 的集合。這是一個(gè)頗為高端的技巧。

當(dāng)我們在對象上調(diào)用 -valueForKey: 的時(shí)候,它可以返回 NSArray,NSSet 或是 NSOrderedSet 的集合代理對象。這個(gè)類沒有實(shí)現(xiàn)通常的 -<Key> 方法,但是它實(shí)現(xiàn)了代理對象所需要使用的很多方法。

如果我們希望一個(gè)類支持通過代理對象的 contacts 鍵返回一個(gè) NSArray,我們可以這樣寫:

- (NSUInteger)countOfContacts;
- (id)objectInContactsAtIndex:(NSUInteger)idx;

這樣做的話,當(dāng)我們調(diào)用 [object valueForKey:@"contacts”] 的時(shí)候,它會返回一個(gè)由這兩個(gè)方法來代理所有調(diào)用方法的 NSArray 對象。這個(gè)數(shù)組支持所有正常的對 NSArray 的調(diào)用。換句話說,調(diào)用者并不知道返回的是一個(gè)真正的 NSArray, 還是一個(gè)代理的數(shù)組。

對于 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)代理對象的性能。

雖然只有特殊情況下我們用這些代理對象才會有意義,但是在這些情況下代理對象非常的有用。想象一下我們有一個(gè)很大的數(shù)據(jù)結(jié)構(gòu),調(diào)用者不需要(一次性)訪問所有的對象。

舉一個(gè)(也許比較做作的)例子說,我們想寫一個(gè)包含有很長一串質(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

我們將會運(yùn)行以下代碼:

Primes *primes = [[Primes alloc] init];
NSLog(@"The last prime is %@", [primes.primes lastObject]);

這將會調(diào)用一次 -countOfPrimes 和一次傳入?yún)?shù) idx 作為最后一個(gè)索引的 -objectInPrimesAtIndex:。為了只取出最后一個(gè)值,它不需要先把所有的數(shù)封裝成 NSNumber 然后把它們都導(dǎo)入 NSArray。

在一個(gè)復(fù)雜一點(diǎn)的例子中,通訊錄編輯器示例 app 用同樣的方法把 C++ std::vector 封裝以來。它詳細(xì)說明了應(yīng)該怎么利用這個(gè)方法。

可變的集合

我們也可以在可變集合(例如 NSMutableArray,NSMutableSet,和 NSMutableOrderedSet)中用集合代理。

訪問這些可變的集合有一點(diǎn)點(diǎn)不同。調(diào)用者在這兒需要調(diào)用以下其中一個(gè)方法:

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;

一個(gè)竅門:我們可以讓一個(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>:

上面提到,這些可變集合代理對象和 KVO 結(jié)合起來也十分強(qiáng)大。KVO 機(jī)制能在這些集合改變的時(shí)候把詳細(xì)的變化放進(jìn) change 字典中。

有批量更新(需要傳入多個(gè)對象)的方法,也有只改變一個(gè)對象的方法。我們推薦選擇相對于給定任務(wù)來說最容易實(shí)現(xiàn)的那個(gè)來寫,雖然我們有一點(diǎn)點(diǎn)傾向于選擇批量更新的那個(gè)。

在實(shí)現(xiàn)這些方法的時(shí)候,我們要對自動和手動的 KVO 之間的差別十分小心。Foundation 默認(rèn)自動發(fā)出十分詳盡的變化通知。如果我們要手動實(shí)現(xiàn)發(fā)送詳細(xì)通知的話,我們得實(shí)現(xiàn)這些:

-willChange:valuesAtIndexes:forKey:
-didChange:valuesAtIndexes:forKey:

或者這些:

-willChangeValueForKey:withSetMutation:usingObjects:
-didChangeValueForKey:withSetMutation:usingObjects:

我們要保證先把自動通知關(guān)閉,否則每次改變 KVO 都會發(fā)出兩次通知。

常見的 KVO 錯(cuò)誤

首先,KVO 兼容是 API 的一部分。如果類的所有者不保證某個(gè)屬性兼容 KVO,我們就不能保證 KVO 正常工作。蘋果文檔里有 KVO 兼容屬性的文檔。例如,NSProgress 類的大多數(shù)屬性都是兼容 KVO 的。

當(dāng)做出改變以后,有些人試著放空的 -willChange-didChange 方法來強(qiáng)制 KVO 的觸發(fā)。KVO 通知雖然會生效,但是這樣做破壞了有依賴于 NSKeyValueObservingOld 選項(xiàng)的觀察者。詳細(xì)來說,這影響了 KVO 對觀察鍵路徑 (key path) 的原生支持。KVO 在觀察鍵路徑 (key path) 時(shí)依賴于 NSKeyValueObservingOld 屬性。

我們也要指出有些集合是不能被觀察的。KVO 旨在觀察關(guān)系 (relationship) 而不是集合。我們不能觀察 NSArray,我們只能觀察一個(gè)對象的屬性——而這個(gè)屬性有可能是 NSArray。舉例說,如果我們有一個(gè) ContactList 對象,我們可以觀察它的 contacts 屬性。但是我們不能向要觀察對象的 -addObserver:forKeyPath:... 傳入一個(gè) NSArray。

相似地,觀察 self 不是永遠(yuǎn)都生效的。而且這不是一個(gè)好的設(shè)計(jì)。

調(diào)試 KVO

你可以在 lldb 里查看一個(gè)被觀察對象的所有觀察信息。

(lldb) po [observedObject observationInfo]

這會打印出有關(guān)誰觀察誰之類的很多信息。

這個(gè)信息的格式不是公開的,我們不能讓任何東西依賴它,因?yàn)樘O果隨時(shí)都可以改變它。不過這是一個(gè)很強(qiáng)大的排錯(cuò)工具。

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

最后提示,KVV 也是 KVC API 的一部分。這是一個(gè)用來驗(yàn)證屬性值的 API,只是它光靠自己很難提供邏輯和功能。

如果我們寫能夠驗(yàn)證值的 model 類的話,我們就應(yīng)該實(shí)現(xiàn) KVV 的 API 來保證一致性。用 KVV 驗(yàn)證 model 類的值是 Cocoa 的慣例。

讓我們在一次強(qiáng)調(diào)一下:KVC 不會做任何的驗(yàn)證,也不會調(diào)用任何 KVV 的方法。那是你的控制器需要做的事情。通過 KVV 實(shí)現(xiàn)你自己的驗(yàn)證方法會保證它們的一致性。

以下是一個(gè)簡單的例子:

- (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í)候,會有機(jī)會去處理名字。

如果我們想讓名字不要有前后的空白字符,我們應(yīng)該把這些邏輯放在 model 對象里面。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è)用法。