在編程中,最常見的就是程序的流程取決于你所使用的各種變量和屬性的值猪贪,根據(jù)變量和屬性的值確定后面運(yùn)行的代碼跷敬,有時(shí)會(huì)檢查對(duì)象是否已加入數(shù)組,或是否已被移除热押,因此西傀,獲取類中屬性的變化是編程中重要部分。
我們有多種方式獲取對(duì)象的改變桶癣,如委托拥褂、通知等。如果需要觀察多個(gè)屬性的變化牙寞,為避免產(chǎn)生大量的代碼饺鹃,最好是使用鍵值觀察(Key Value Observing,簡(jiǎn)稱KVO)间雀,這也是Apple在自己的軟件中大量使用的一種尤慰。
使用鍵值觀察跟蹤單個(gè)屬性或集合(如數(shù)組)的變化非常高效,它只需要在觀察者方法中添加代碼雷蹂,不需要修改被觀察文件內(nèi)的代碼伟端,這一點(diǎn)和委托、通知不同匪煌。但需要注意的是责蝠,鍵值觀察(KVO)是建立在鍵值編碼(Key Value Coding,簡(jiǎn)稱KVC)的基礎(chǔ)上萎庭,也就是說任何你想使用KVO觀察的屬性必須符合鍵值編碼霜医。
KVC和KVO提供了一個(gè)強(qiáng)大高效的方式來編寫代碼,學(xué)習(xí)KVO前必須先掌握KVC驳规,所以下面我們結(jié)合demo來學(xué)習(xí)KVC肴敛。在這個(gè)demo中所有結(jié)果將直接在控制臺(tái)輸出,沒有創(chuàng)建用戶界面。
1. 創(chuàng)建應(yīng)用
啟動(dòng)Xcode医男,點(diǎn)擊File > New > Project…砸狞,選擇iOS > Application > Single View Application模板,點(diǎn)擊Next镀梭;在Product Name一欄填寫KVC&KVODemo
刀森,點(diǎn)擊Next;選擇文件位置报账,點(diǎn)擊Create創(chuàng)建工程研底。
2. 鍵值編碼
假設(shè)我們有一個(gè)NSString
類型的firstName
的屬性,我們想把Donald
賦值給屬性透罢,我們可以使用下面兩種方式之一榜晦。
self.firstName = @"Donald"; // 1
_firstName = @"Donald"; // 2
上面的代碼我們非常熟悉,1直接為屬性賦值羽圃,2直接給實(shí)例變量賦值芽隆。如果使用KVC設(shè)值,代碼如下:
[self setValue:@"Donald" forKey:@"firstName"];
你會(huì)發(fā)現(xiàn)使用KVC和詞典中設(shè)值或?qū)?biāo)量值和結(jié)構(gòu)值轉(zhuǎn)換為NSValue
非常類似统屈。再舉一例,下面代碼3使用設(shè)值方法設(shè)值牙躺,4使用KVC模式設(shè)值愁憔。
[someObject.someProperty setText:@"This is a text"]; // 3
[self setValue:@"This is a text" forKey:@"someObject.someProperty.text"]; // 4
在第一個(gè)示例中,我們用KVC替代直接賦值孽拷,在第二個(gè)示例中吨掌,我們用KVC替代訪問器方法設(shè)值。使用KVC時(shí)我們只需要將值與Key
或KeyPath
匹配就可以脓恕,使用字符串間接把值賦給屬性膜宋。如果需要獲取屬性的值,可用下面方式:
NSLog(@"%@",[self valueForKey:@"firstName"]);
鍵值編碼機(jī)制是由一個(gè)NSKeyValueCoding
非正式協(xié)議定義的炼幔,NSObject
實(shí)現(xiàn)了這個(gè)協(xié)議秋茫,所以我們繼承NSObject
才能讓我們的類獲得KVC能力。理論上乃秀,如果你的類遵守NSKeyValueCoding
協(xié)議肛著,也可以自己實(shí)現(xiàn)KVC的細(xì)節(jié),這樣做完全行得通跺讯,但這樣太不值得了枢贿,也太占用時(shí)間了。
打開Xcode刀脏,點(diǎn)擊File > New > File…局荚,或使用快捷鍵(?+N)創(chuàng)建一個(gè)類。在彈出窗口中,選擇iOS > Source > Cocoa Touch Class模板耀态,點(diǎn)擊Next轮傍;類名稱為Children
,父類為NSObject
茫陆,點(diǎn)擊Next金麸;選擇文件位置,點(diǎn)擊Create創(chuàng)建文件簿盅。
進(jìn)入Children.h
文件挥下,添加兩個(gè)屬性,一個(gè)是firstName
桨醋,一個(gè)是age
棚瘟,我們將使用這兩個(gè)屬性展示KVC的主要特性。更新后代碼如下:
@interface Children : NSObject
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, assign) NSUInteger age;
@end
進(jìn)入Children.m
文件初始化上面兩個(gè)屬性喜最。
@implementation Children
- (instancetype)init
{
self = [super init];
if (self)
{
_firstName = @"";
_age = 0;
}
return self;
}
@end
進(jìn)入ViewController.m
文件偎蘸,導(dǎo)入Children.h
,聲明兩個(gè)Children
類型的屬性瞬内。代碼如下:
#import "ViewController.h"
#import "Children.h"
@interface ViewController ()
@property (nonatomic, strong) Children *child1;
@property (nonatomic, strong) Children *child2;
@end
在viewDidLoad
方法中迷雪,初始化child1
對(duì)象,使用KVC方法先設(shè)值虫蝶、后取值并輸出到控制臺(tái)章咧。
- (void)viewDidLoad
{
[super viewDidLoad];
// child1
self.child1 = [Children new];
// 1.使用KVC設(shè)值
[self.child1 setValue:@"Jr" forKey:@"firstName"];
[self.child1 setValue:[NSNumber numberWithUnsignedInteger:39] forKey:@"age"];
// 2. 取值 輸出到控制臺(tái)
NSString *childFirstName = [self.child1 valueForKey:@"firstName"];
NSUInteger child1Age = [[self.child1 valueForKey:@"age"] unsignedIntegerValue];
NSLog(@"%@,%lu",childFirstName,child1Age);
}
在1中使用setValue: forKey:
為屬性設(shè)值。需要注意的是age
是數(shù)字能真,因此不能直接作為參數(shù)赁严,需要轉(zhuǎn)換為NSNumber
類型,另外鍵(Key)的字符串必須和屬性中的名稱一致粉铐,否則運(yùn)行時(shí)app會(huì)崩潰疼约,彈出Terminating app due to uncaught exception 'NSUnknownKeyException',提示。在2中蝙泼,使用valueForKey:
取值程剥,輸出到控制臺(tái)。
Jr,39
目前為止汤踏,我們已經(jīng)學(xué)習(xí)了如何編寫符合KVC的代碼倡缠,如何使用KVC設(shè)值和取值,以及Key
寫錯(cuò)會(huì)如何【セ睿現(xiàn)在開始學(xué)習(xí)一下如何使用KeyPath
昙沦。首先進(jìn)入Children.h
文件,添加一個(gè)Children
類型的屬性载荔。
@interface Children : NSObject
···
@property (nonatomic, strong) Children *child;
@end
返回到ViewController.m
盾饮,在viewDidLoad
方法中,初始化child2
并設(shè)值,最后初始化child
屬性丘损。
- (void)viewDidLoad
{
···
// child2
self.child2 = [Children new];
[self.child2 setValue:@"Ivanka" forKey:@"firstName"];
[self.child2 setValue:[NSNumber numberWithUnsignedInteger:35] forKey:@"age"];
self.child2.child = [Children new];
}
現(xiàn)在使用setValue: forKeyPath:
為child
屬性設(shè)值普办,這里的鍵是一個(gè)使用點(diǎn)語法的字符串@"child.firstName"
。
- (void)viewDidLoad
{
...
[self.child2 setValue:@"Eric" forKeyPath:@"child.firstName"];
[self.child2 setValue:[NSNumber numberWithUnsignedInteger:33] forKeyPath:@"child.age"];
NSLog(@"%@,%lu",self.child2.child.firstName, self.child2.child.age);
}
最后使用NSLog
測(cè)試設(shè)值是否成功徘钥。輸出是:
Eric,33
valueForKey:
和valueForKeyPath:
是在NSKeyValueCoding
非正式協(xié)議中定義的方法衔蹲,兩者默認(rèn)由根類NSObject
實(shí)現(xiàn),是KVC框架的一部分呈础。
objectForKey:
是由NSDictionary
提供提取對(duì)應(yīng)鍵的值的方法舆驶。雖然在詞典中使用
valueForKey:
也可以提取到值,但當(dāng)key
字符串以@
開頭時(shí)會(huì)遇到問題而钞。所以在詞典中使用objectForKey:
沙廉,在KVC中使用valueForKey:
和valueForKeyPath:
。
3. 鍵值觀察
我們已經(jīng)掌握了KVC臼节,現(xiàn)在開始學(xué)習(xí)KVO撬陵。以下是實(shí)現(xiàn)KVO的步驟:
- 使用
addObserver: forKeyPath: options: context:
方法注冊(cè)為觀察者,用于觀察其他類的屬性网缝。 - 觀察者必須實(shí)現(xiàn)
observerValueForKeyPath: ofObject: change: context:
方法以接收屬性變化通知巨税。 - 使用
removeObserver: forKeyPath: context:
方法移除觀察者。
3.1 觀察單個(gè)屬性
進(jìn)入ViewController.m
粉臊,在實(shí)現(xiàn)部分添加viewWillAppear:
方法草添,在viewWillAppear:
方法內(nèi)添加firstName
和age
屬性為觀察對(duì)象。
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.child1 addObserver:self forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[self.child1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
}
上面添加觀察者方法中各參數(shù)含義如下:
-
addObserver:
注冊(cè)成為觀察者维费,以便接收KVO通知,通常為self
促王,該對(duì)象必須實(shí)現(xiàn)observerValueForKeyPath: ofObject: change: context:
以接收屬性變化通知犀盟。 -
keyPath:
要觀察的屬性字符串,必須和屬性一致蝇狼,不能為空阅畴。 -
options:
用來指定通知詞典中應(yīng)包含值類型。如果參數(shù)是NSKeyValueObservingOptionNew
迅耘,詞典包含新產(chǎn)生的值贱枣;如果參數(shù)是NSKeyValueObservingOptionOld
,詞典包含變化前的值颤专;如果參數(shù)是數(shù)字0
纽哥,詞典不包括任何值;如果需要change
詞典同時(shí)包括新產(chǎn)生值和變化前的舊值栖秕,可以像上面代碼一樣使用|
春塌,即或運(yùn)算符;任何時(shí)候都可以使用[object valueForKey:<Key>]
方法獲取屬性變化產(chǎn)生的新值。 -
context:
這是一個(gè)指針只壳,可用做我們觀察到的屬性更改的唯一標(biāo)志符俏拱,經(jīng)常設(shè)置為NULL
,后面會(huì)詳細(xì)說明吼句。
現(xiàn)在我們已經(jīng)可以觀察到firstName
和age
兩個(gè)屬性的的變化锅必,KVO觀察到每一個(gè)觀察對(duì)象的變化都會(huì)調(diào)用observerValueForKeyPath: ofObject: change: context:
方法,如果觀察多個(gè)屬性的變化惕艳,觀察方法內(nèi)if
語句可能很長(zhǎng)搞隐,下面實(shí)現(xiàn)observerValueForKeyPath: ofObject: change: context:
方法。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"firstName"])
{
NSLog(@"The name of the child was changed.\n %@",change);
}
else if ([keyPath isEqualToString:@"age"])
{
NSLog(@"The new value is %@,The old value is %@",[change valueForKey:NSKeyValueChangeNewKey],[change valueForKey:NSKeyValueChangeOldKey]);
}
}
在上面代碼中尔艇,我們根據(jù)參數(shù)keyPath
判斷哪一個(gè)屬性改變了尔许,之后輸出。在輸出時(shí)终娃,可以直接輸出詞典change
味廊,也可以用valueForKey:
獲取詞典中的值,此處的鍵用NSKeyValueChangeNewKey
或NSKeyValueChangeOldKey
棠耕,前者獲取新產(chǎn)生的值余佛,后者獲取改變前的舊值。現(xiàn)在在viewwillAppear:
底部添加下面代碼來驗(yàn)證是否可以觀察到屬性變化窍荧。
- (void)viewWillAppear:(BOOL)animated
{
...
// 添加觀察者后改變值 驗(yàn)證是否可以觀察到值變化
[self.child1 setValue:@"Tiffany" forKey:@"firstName"];
[self.child1 setValue:[NSNumber numberWithUnsignedInteger:23] forKey:@"age"];
}
運(yùn)行辉巡,輸出內(nèi)容為:
The name of the child was changed.
{
kind = 1;
new = Tiffany;
old = Jr;
}
The new value is 23,The old value is 39
你可以從change
詞典中提取你需要的值,有了KVO觀察屬性變化變的如此簡(jiǎn)單∪锿耍現(xiàn)在在viewWillAppear:
方法中添加觀察者郊楣,觀察child2
對(duì)象的屬性變化,隨后為age
設(shè)值瓤荔。
- (void)viewWillAppear:(BOOL)animated
{
...
// 觀察child2屬性變化 設(shè)值
[self.child2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[self.child2 setValue:[NSNumber numberWithUnsignedInteger:64] forKey:@"age"];
}
運(yùn)行后輸出如下:
The name of the child was changed.
{
kind = 1;
new = Tiffany;
old = Jr;
}
The new value is 23,The old value is 39
The new value is 64,The old value is 35
正如看到的一樣净蚤,我們收到兩個(gè)age
屬性的改變通知,盡管我們自己可以區(qū)分出每一個(gè)通知來自哪一個(gè)對(duì)象屬性的改變输硝,但在程序中今瀑,目前我們無法對(duì)此進(jìn)行區(qū)分莹汤。為解決這個(gè)問題稽穆,我們將使用addObserver: forKeyPath: options: context:
方法中的context
參數(shù)倾贰。context
參數(shù)一般使用下面聲明方法盗温。
static void *XXContext = &XXContext;
表示一個(gè)靜態(tài)變量存放著它自己的指針袄琳,也就是它自己什么也沒有葱色。因?yàn)橐?code>addObserver: forKeyPath: options: context:和observerValueForKeyPath: ofObject: change: context:
兩個(gè)方法中使用context
祟霍,這里的context
聲明為靜態(tài)全局變量哼凯。
在ViewController.m
實(shí)現(xiàn)前添加下面兩個(gè)聲明褒翰。
@end
static void *child1Context = &child1Context;
static void *child2Context = &child2Context;
@implementation ViewController
你也可以把context
聲明為屬性如蚜,但聲明為全局變量更為簡(jiǎn)單⊙购悖現(xiàn)在修改viewWillAppear:
方法中的添加觀察者方法,將context
參數(shù)中的NULL
替換為剛聲明的全局變量错邦。
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.child1 addObserver:self forKeyPath:@"firstName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:child1Context];
[self.child1 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:child1Context];
// 添加觀察者后改變值 驗(yàn)證是否可以觀察到值變化
[self.child1 setValue:@"Tiffany" forKey:@"firstName"];
[self.child1 setValue:[NSNumber numberWithUnsignedInteger:23] forKey:@"age"];
// 觀察child2屬性變化 設(shè)值
[self.child2 addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:child2Context];
[self.child2 setValue:[NSNumber numberWithUnsignedInteger:64] forKey:@"age"];
}
最后修改observerValueForKeyPath: ofObject: change: context:
方法探赫,以便區(qū)分出通知來自哪一個(gè)對(duì)象屬性的變化。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
...
// 使用context后
if (context == child1Context)
{
if ([keyPath isEqualToString:@"firstName"])
{
NSLog(@"The name of the FIRST child was changed.\n %@",change);
}
else if ([keyPath isEqualToString:@"age"])
{
NSLog(@"The new value of the FIRST child is %@,The new value of the FIRST child is %@",[change valueForKey:NSKeyValueChangeNewKey],[change valueForKey:NSKeyValueChangeOldKey]);
}
}
else if (context == child2Context)
{
if ([keyPath isEqualToString:@"age"])
{
NSLog(@"The new value of the SECOND child is %@,The new value of the SECOND child is %@",[change valueForKey:NSKeyValueChangeNewKey],[change valueForKey:NSKeyValueChangeOldKey]);
}
}
}
3.2 注冊(cè)相互影響的鍵
在許多情況下撬呢,一個(gè)屬性的值取決另一個(gè)對(duì)象中的一個(gè)或多個(gè)其他屬性的值伦吠。如果一個(gè)屬性的值改變,那么派生屬性的值也應(yīng)該改變魂拦。例如:姓名由姓和名兩個(gè)屬性組成毛仪,當(dāng)其中任何一個(gè)屬性變化時(shí),姓名屬性都要得到改變的通知。
進(jìn)入Children.h
芯勘,添加NSString
類型的fullName
屬性和lastName
兩個(gè)屬性箱靴。
@interface Children : NSObject
...
@property (nonatomic, strong) NSString *fullName;
@property (nonatomic, strong) NSString *lastName;
@end
進(jìn)入Children.m
,初始化剛聲明的屬性衡怀,fullName
由firstName
和lastName
組成安疗。
- (instancetype)init
{
self = [super init];
if (self)
{
...
_lastName = @"";
}
return self;
}
- (NSString *)fullName
{
return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}
當(dāng)firstName
和lastName
屬性變化時(shí),必須通知fullName
屬性的應(yīng)用程序荐类,因?yàn)樗鼈儠?huì)影響fullName
屬性的值∮窆蓿可以通過實(shí)現(xiàn)類方法keyPathsForValuesAffecting<key>
來獲取哪些屬性會(huì)影響<key>
屬性,這里的<key>
為fullName
吊输,首字母要大寫饶号。在Children.m
中添加以下類方法:
+ (NSSet *)keyPathsForValuesAffectingFullName
{
return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}
進(jìn)入ViewController.m
文件璧亚,在viewWillAppear:
方法中添加觀察者脂信,觀察fullname
屬性狰闪,之后修改lastName
和firstName
屬性。
- (void)viewWillAppear:(BOOL)animated
{
...
// 添加觀察者 觀察fullName屬性 修改firstName lastName
[self.child1 addObserver:self forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:child1Context];
self.child1.lastName = @"Trump";
self.child1.firstName = @"Ivana";
}
在observerValueForKeyPath: ofObject: change: context:
方法中幔欧,觀察到fullname
屬性變化時(shí)進(jìn)行輸出礁蔗。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
...
// 使用context后
if (context == child1Context)
{
...
else if ([keyPath isEqualToString:@"fullName"])
{
NSLog(@"The full name of First child was change.\n %@",change);
}
}
...
}
現(xiàn)在你可以運(yùn)行下浴井,可以輸出fullname
屬性的變化。
The full name of First child was change.
{
kind = 1;
new = "Tiffany Trump";
old = "Tiffany ";
}
The full name of First child was change.
{
kind = 1;
new = "Ivana Trump";
old = "Tiffany Trump";
}
The name of the FIRST child was changed.
{
kind = 1;
new = Ivana;
old = Tiffany;
}
3.3 觀察數(shù)組
NSArray
是KVC和KVO中的一種特殊情況洪囤,想要觀察到數(shù)組的變化需要做一些額外的工作瘤缩。事實(shí)上伦泥,有很多關(guān)于數(shù)組的細(xì)節(jié),但在這里铐殃,我們只講解一些基礎(chǔ)的富腊、重要的內(nèi)容域帐。因?yàn)閿?shù)組不符合KVC肖揣,因此觀察數(shù)組不像觀察上面示例中的屬性那么簡(jiǎn)單龙优。我們要實(shí)現(xiàn)一些關(guān)于數(shù)組的方法以便使數(shù)組符合KVC,進(jìn)而可以使用KVO觀察數(shù)組的變化野舶。
這里我們將討論可變數(shù)組平道,不可變數(shù)組與可變數(shù)組類似供炼,只是需要實(shí)現(xiàn)的方法少一些。假設(shè)我們有一個(gè)可變數(shù)組myArray
闸衫,這里需要實(shí)現(xiàn)的方法與數(shù)組的插入诽嘉、移除含懊、計(jì)數(shù)類似岔乔,不同之處在于數(shù)組的名稱,需要實(shí)現(xiàn)方法如下:
- countOfMyArray
- objectInMyArrayAtIndex:
- insertObject:inMyArrayAtIndex:
- removeObjectFromMyArrayAtIndex:
這些方法都很熟悉嘿歌,不同的是我們用數(shù)組的名稱替換里面名稱宙帝。如果是不可變數(shù)組募闲,只需要取消實(shí)現(xiàn)最后兩個(gè)方法浩螺。
讓數(shù)組符合KVC有好的一方面要出,也有不利的一方面。好處是Xcode會(huì)對(duì)數(shù)組名建議補(bǔ)全或颊;壞的一方面是類中每一個(gè)想使用KVC觀察的數(shù)組都要實(shí)現(xiàn)這些方法囱挑,會(huì)產(chǎn)生大量代碼格了。為了避免產(chǎn)生大量重復(fù)代碼盛末,我們可以創(chuàng)建一個(gè)新的類悄但,類內(nèi)只包含一個(gè)可變數(shù)組檐嚣,在這個(gè)數(shù)組內(nèi)實(shí)現(xiàn)這些方法讓這個(gè)數(shù)組符合KVC,這樣在其他類中使用這個(gè)類的實(shí)例對(duì)象嗡贺。這樣的好處是:讓數(shù)組符合KVC诫睬,必須實(shí)現(xiàn)的方法只需要實(shí)現(xiàn)一次帕涌,這個(gè)類可以重復(fù)使用蚓曼。你可以理解為這是一個(gè)高級(jí)版本的數(shù)組纫版。
現(xiàn)在添加一個(gè)新類其弊,點(diǎn)擊File > New > File…,選取iOS > Source > Cocoa Touch Class模板末秃,點(diǎn)擊Next练慕;類名稱為KVCMutableArray
铃将,父類為NSObject
哑梳,點(diǎn)擊Next鸠真;選擇文件位置,點(diǎn)擊Create創(chuàng)建文件锡垄。
進(jìn)入KVCMutableArray.h
文件货岭,聲明一個(gè)可變數(shù)組及一些方法以便數(shù)組符合KVC千贯。
@interface KVCMutableArray : NSObject
@property (nonatomic, strong) NSMutableArray *array;
- (NSUInteger)countOfArray;
- (id)objectInArrayAtIndex:(NSUInteger)index;
- (void)insertObject:(id)object inArrayAtIndex:(NSUInteger)index;
- (void)removeObjectFromArrayAtIndex:(NSUInteger)index;
- (void)replaceObjectInArrayAtIndex:(NSUInteger)index withObject:(id)object;
@end
在上面insertObject: inArrayAtIndex:
方法中搔谴,object
對(duì)象這里設(shè)定為id
類型己沛,以便其他類可以使用。進(jìn)入KVCMutableArray.m
垮卓,添加init
方法粟按,初始化array
,實(shí)現(xiàn)頭文件中聲明的方法灭将。
@implementation KVCMutableArray
- (instancetype)init
{
self = [super init];
if (self)
{
_array = [NSMutableArray new];
}
return self;
}
- (NSUInteger)countOfArray
{
return self.array.count;
}
- (id)objectInArrayAtIndex:(NSUInteger)index
{
return [self.array objectAtIndex:index];
}
- (void)insertObject:(id)object inArrayAtIndex:(NSUInteger)index
{
[self.array insertObject:object atIndex:index];
}
- (void)removeObjectFromArrayAtIndex:(NSUInteger)index
{
[self.array removeObjectAtIndex:index];
}
- (void)replaceObjectInArrayAtIndex:(NSUInteger)index withObject:(id)object
{
[self.array replaceObjectAtIndex:index withObject:object];
}
@end
到目前我們已經(jīng)創(chuàng)建了一個(gè)符合KVC的數(shù)組庙曙。
現(xiàn)在我們要在Children.h
中添加一個(gè)可變數(shù)組捌朴,數(shù)組內(nèi)包括姓名张抄,用KVO觀察數(shù)組內(nèi)容的變化署惯。在這里我們應(yīng)該使用KVCMutableArray
類型的屬性,而不是系統(tǒng)提供的默認(rèn)數(shù)組安岂,進(jìn)入Children.h
,導(dǎo)入KVCMutableArray.h
文件蜕依,添加新的屬性样眠。
#import <Foundation/Foundation.h>
#import "KVCMutableArray.h"
@interface Children : NSObject
...
@property (nonatomic, strong) KVCMutableArray *cousins;
@end
在Children.m
中初始化剛聲明的對(duì)象檐束。
- (instancetype)init
{
self = [super init];
if (self)
{
...
_cousins = [KVCMutableArray new];
}
return self;
}
進(jìn)入ViewController.m
文件被丧,在viewWillAppear:
方法底部添加如下代碼:
- (void)viewWillAppear:(BOOL)animated
{
...
// 對(duì)數(shù)組進(jìn)行觀察
[self.child1 addObserver:self forKeyPath:@"cousins.array" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:NULL];
[self.child1.cousins insertObject:@"Antony" inArrayAtIndex:0];
[self.child1.cousins insertObject:@"Julia" inArrayAtIndex:1];
[self.child1.cousins replaceObjectInArrayAtIndex:0 withObject:@"Ben"];
}
在observerValueForKeyPath: ofObject: change: context:
方法中處理接收到通知甥桂。
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
...
else if ([keyPath isEqualToString:@"cousins.array"] && [object isKindOfClass:[Children class]])
{
NSLog(@"cousins.array %@",change);
}
else
{
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
這里除了使用keyPath
進(jìn)行判斷還可以額外進(jìn)行類判斷黄选,當(dāng)父類也在觀察屬性時(shí)會(huì)有幫助办陷。最后,如果沒有合適的context
或keyPath
民镜,把它交給父類來處理制圈,可能是父類也在觀察同一個(gè)屬性。
運(yùn)行app病附,輸出結(jié)果證明觀察數(shù)組成功完沪。
cousins.array {
indexes = "<_NSCachedIndexSet: 0x6000000394e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 2;
new = (
Antony
);
}
cousins.array {
indexes = "<_NSCachedIndexSet: 0x600000039500>[number of indexes: 1 (in 1 ranges), indexes: (1)]";
kind = 2;
new = (
Julia
);
}
cousins.array {
indexes = "<_NSCachedIndexSet: 0x6000000394e0>[number of indexes: 1 (in 1 ranges), indexes: (0)]";
kind = 4;
new = (
Ben
);
old = (
Antony
);
}
3.4 手動(dòng)發(fā)送通知
默認(rèn)情況下覆积,KVO觀察到屬性變化系統(tǒng)會(huì)自動(dòng)發(fā)送通知宽档,但在某些情況下吗冤,你可能需要控制何時(shí)發(fā)送通知椎瘟。例如:在某些情況下不需要發(fā)送通知,或?qū)⒍鄠€(gè)改變合并為一個(gè)通知發(fā)送煌妈。手動(dòng)發(fā)送通知提供了執(zhí)行此操作的方法璧诵。
手動(dòng)和自動(dòng)通知并不互斥腮猖,已經(jīng)存在自動(dòng)通知的類內(nèi)也可以添加手動(dòng)通知澈缺。你可以通過重寫由NSObject
實(shí)現(xiàn)的automaticallyNotifiesObserversForKey:
類方法來控制特定屬性的通知發(fā)送姐赡,這個(gè)方法的參數(shù)key
就是想要手動(dòng)控制通知的屬性柠掂,這個(gè)方法返回值類型是BOOL
類型涯贞,想要手動(dòng)控制通知的屬性在重寫這個(gè)類方法時(shí)返回NO
宋渔,其他屬性由超類來處理皇拣。
假設(shè)我們現(xiàn)在不想接收firstName
屬性的變化,進(jìn)入Children.m
文件毫深,在實(shí)現(xiàn)部分添加下面類方法毒姨。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"firstName"])
{
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
上面的方法非常簡(jiǎn)單弧呐,暫停firstName
屬性的自動(dòng)通知泉懦;在else
部分崩哩,使用超類調(diào)用相同方法邓嘹,以便讓iOS處理所有未在上面顯式添加的屬性汹押。
現(xiàn)在運(yùn)行app棚贾,你會(huì)發(fā)現(xiàn)所有firstName
屬性的變化都沒有輸出∮茏郏現(xiàn)在只是能夠停止特定鍵對(duì)應(yīng)屬性變化的通知鼻疮,還不能稱為手動(dòng)發(fā)送通知判沟。
想要手動(dòng)發(fā)送通知挪哄,需要添加willChangeValueForKey:
和didChangeValueForKey:
方法迹炼。步驟如下:
- 調(diào)用
willChangeValueForKey:
方法。 - 修改所觀察屬性的值吟秩。
- 調(diào)用
didChangeValueForKey:
方法绽淘。
進(jìn)入ViewController.m
文件沪铭,在viewWillAppear:
方法中找到[self.child1 setValue:@"Tiffany" forKey:@"firstName"];
這一行代碼杀怠,并用下面三行代碼替換赔退。
- (void)viewWillAppear:(BOOL)animated
{
...
// 添加觀察者后改變值 驗(yàn)證是否可以觀察到值變化
[self.child1 willChangeValueForKey:@"firstName"];
[self.child1 setValue:@"Tiffany" forKey:@"firstName"];
[self.child1 didChangeValueForKey:@"firstName"];
...
}
現(xiàn)在運(yùn)行app硕旗,firstName
屬性的改變會(huì)在控制臺(tái)輸出漆枚,也就是我們已經(jīng)成功手動(dòng)發(fā)送通知墙基。
The name of the FIRST child was changed.
{
kind = 1;
new = Tiffany;
old = Jr;
}
事實(shí)上残制,通知是在調(diào)用didChangeValueForKey:
方法后發(fā)送的痘拆。如果不想在改變屬性值后立即發(fā)送通知纺蛆,可以在改變屬性后任何想要發(fā)送通知的位置調(diào)用didChangeValueForKey:
方法桥氏。例如這個(gè)demo中字支,你可以把[self.child1 didChangeValueForKey:@"firstName"];
放在程序行代碼的最底部,控制臺(tái)內(nèi)容輸出順序?qū)?huì)發(fā)生變化栗菜,這里不再詳細(xì)說明疙筹。
如果單個(gè)操作導(dǎo)致多個(gè)鍵改變而咆,則必須嵌套更改通知暴备。如下:
[self.child1 willChangeValueForKey:@"firstName"]; [self.child1 willChangeValueForKey:@"property"]; self.child1.firstName = @"First"; // 1 不能觀察到 self.child1.firstName = @"Second"; // 2 可以觀察到 self.child1.property = @"xxx"; [self.child1 didChangeValueForKey:@"firstName"]; [self.child1 didChangeValueForKey:@"property"];
可以把多個(gè)手動(dòng)通知嵌套在一起涯捻,每個(gè)手動(dòng)通知只能觀察到鍵最新一次的改變汰瘫。如上面代碼,只有2可以觀察到改變对省,1的改變不能觀察到蒿涎。
最后一定要記得移除觀察者劳秋。如果視圖控制器釋放前沒有移除觀察者玻淑,釋放時(shí)app會(huì)崩潰补履。一般添加觀察者在viewDidLoad
方法箫锤、viewWillAppear:
中谚攒,移除觀察者可以在observerValueForKeyPath: ofObject: change: context:
處理完通知后馏臭,或viewWillDisappear:
方法中位喂,也可以在dealloc
方法中塑崖。在這個(gè)demo中规婆,我們?cè)?code>viewWillDisappear:移除觀察者抒蚜。
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// 移除所有觀察者
[self.child1 removeObserver:self forKeyPath:@"firstName" context:child1Context];
[self.child1 removeObserver:self forKeyPath:@"age" context:child1Context];
[self.child1 removeObserver:self forKeyPath:@"fullName" context:child1Context];
[self.child1 removeObserver:self forKeyPath:@"cousins.array" context:NULL];
[self.child2 removeObserver:self forKeyPath:@"age" context:child2Context];
}
每一個(gè)addObserver: forKeyPath: options: context:
必須對(duì)應(yīng)一個(gè)removeObserver: forKeyPath: context:
掘鄙,KVO沒有辦法判斷當(dāng)前控制器是否被注冊(cè)為觀察者,并且移除不存在的觀察者嗡髓,app也會(huì)崩潰操漠。
總結(jié)
鍵值觀察提供了一種允許對(duì)象在其他類屬性變化時(shí)獲得通知的機(jī)制。對(duì)于應(yīng)用程序中模型層和控制器層通信特別有用饿这∽腔铮控制器對(duì)象通常用來觀察模型對(duì)象的屬性,并且視圖對(duì)象也可以通過控制器對(duì)象觀察模型對(duì)象的屬性嚣鄙。此外,模型對(duì)象可以觀察其他模型對(duì)象串结,也可以觀察自身哑子。
鍵值觀察和鍵值編碼都是一種幫助建立更強(qiáng)大、更靈活肌割、更高效的應(yīng)用卧蜓,可能剛接觸時(shí)覺得很奇特,最后你會(huì)感覺這些很容易掌握声功。
這篇文章只介紹了KVC烦却、KVO的用法,如果你想要了解KVC先巴、KVO的底層原理其爵,請(qǐng)查看我的另一篇文章:KVC冒冬、KVO的本質(zhì)。
文件名稱:KVC&KVODemo
源碼地址:https://github.com/pro648/BasicDemos-iOS
參考資料: