在編程中,最常見的就是程序的流程取決于你所使用的各種變量和屬性的值,根據(jù)變量和屬性的值確定后面運行的代碼,有時會檢查對象是否已加入數(shù)組,或是否已被移除,因此,獲取類中屬性的變化是編程中重要部分。
我們有多種方式獲取對象的改變,如委托、通知等。如果需要觀察多個屬性的變化,為避免產(chǎn)生大量的代碼,最好是使用鍵值觀察(Key Value Observing,簡稱KVO),這也是Apple在自己的軟件中大量使用的一種。
使用鍵值觀察跟蹤單個屬性或集合(如數(shù)組)的變化非常高效,它只需要在觀察者方法中添加代碼,不需要修改被觀察文件內(nèi)的代碼,這一點和委托、通知不同。但需要注意的是,鍵值觀察(KVO)是建立在鍵值編碼(Key Value Coding,簡稱KVC)的基礎(chǔ)上,也就是說任何你想使用KVO觀察的屬性必須符合鍵值編碼。
KVC和KVO提供了一個強大高效的方式來編寫代碼,學(xué)習(xí)KVO前必須先掌握KVC,所以下面我們結(jié)合demo來學(xué)習(xí)KVC。在這個demo中所有結(jié)果將直接在控制臺輸出,沒有創(chuàng)建用戶界面。
1. 創(chuàng)建應(yīng)用
啟動Xcode,點擊File > New > Project…,選擇iOS > Application > Single View Application模板,點擊Next;在Product Name一欄填寫KVC&KVODemo
,點擊Next;選擇文件位置,點擊Create創(chuàng)建工程。
2. 鍵值編碼
假設(shè)我們有一個NSString
類型的firstName
的屬性,我們想把Donald
賦值給屬性,我們可以使用下面兩種方式之一。
self.firstName = @"Donald"; // 1
_firstName = @"Donald"; // 2
上面的代碼我們非常熟悉,1直接為屬性賦值,2直接給實例變量賦值。如果使用KVC設(shè)值,代碼如下:
[self setValue:@"Donald" forKey:@"firstName"];
你會發(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
在第一個示例中,我們用KVC替代直接賦值,在第二個示例中,我們用KVC替代訪問器方法設(shè)值。使用KVC時我們只需要將值與Key
或KeyPath
匹配就可以,使用字符串間接把值賦給屬性。如果需要獲取屬性的值,可用下面方式:
NSLog(@"%@",[self valueForKey:@"firstName"]);
鍵值編碼機制是由一個NSKeyValueCoding
非正式協(xié)議定義的,NSObject
實現(xiàn)了這個協(xié)議,所以我們繼承NSObject
才能讓我們的類獲得KVC能力。理論上,如果你的類遵守NSKeyValueCoding
協(xié)議,也可以自己實現(xiàn)KVC的細(xì)節(jié),這樣做完全行得通,但這樣太不值得了,也太占用時間了。
打開Xcode,點擊File > New > File…,或使用快捷鍵(?+N)創(chuàng)建一個類。在彈出窗口中,選擇iOS > Source > Cocoa Touch Class模板,點擊Next;類名稱為Children
,父類為NSObject
,點擊Next;選擇文件位置,點擊Create創(chuàng)建文件。
進(jìn)入Children.h
文件,添加兩個屬性,一個是firstName
,一個是age
,我們將使用這兩個屬性展示KVC的主要特性。更新后代碼如下:
@interface Children : NSObject
@property (nonatomic, strong) NSString *firstName;
@property (nonatomic, assign) NSUInteger age;
@end
進(jìn)入Children.m
文件初始化上面兩個屬性。
@implementation Children
- (instancetype)init
{
self = [super init];
if (self)
{
_firstName = @"";
_age = 0;
}
return self;
}
@end
進(jìn)入ViewController.m
文件,導(dǎo)入Children.h
,聲明兩個Children
類型的屬性。代碼如下:
#import "ViewController.h"
#import "Children.h"
@interface ViewController ()
@property (nonatomic, strong) Children *child1;
@property (nonatomic, strong) Children *child2;
@end
在viewDidLoad
方法中,初始化child1
對象,使用KVC方法先設(shè)值、后取值并輸出到控制臺。
- (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. 取值 輸出到控制臺
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)的字符串必須和屬性中的名稱一致,否則運行時app會崩潰,彈出Terminating app due to uncaught exception 'NSUnknownKeyException',提示。在2中,使用valueForKey:
取值,輸出到控制臺。
Jr,39
目前為止,我們已經(jīng)學(xué)習(xí)了如何編寫符合KVC的代碼,如何使用KVC設(shè)值和取值,以及Key
寫錯會如何。現(xiàn)在開始學(xué)習(xí)一下如何使用KeyPath
。首先進(jìn)入Children.h
文件,添加一個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è)值,這里的鍵是一個使用點語法的字符串@"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
測試設(shè)值是否成功。輸出是:
Eric,33
valueForKey:
和valueForKeyPath:
是在NSKeyValueCoding
非正式協(xié)議中定義的方法,兩者默認(rèn)由根類NSObject
實現(xiàn),是KVC框架的一部分。
objectForKey:
是由NSDictionary
提供提取對應(yīng)鍵的值的方法。雖然在詞典中使用
valueForKey:
也可以提取到值,但當(dāng)key
字符串以@
開頭時會遇到問題。所以在詞典中使用objectForKey:
,在KVC中使用valueForKey:
和valueForKeyPath:
。
3. 鍵值觀察
我們已經(jīng)掌握了KVC,現(xiàn)在開始學(xué)習(xí)KVO。以下是實現(xiàn)KVO的步驟:
- 使用
addObserver: forKeyPath: options: context:
方法注冊為觀察者,用于觀察其他類的屬性。 - 觀察者必須實現(xiàn)
observerValueForKeyPath: ofObject: change: context:
方法以接收屬性變化通知。 - 使用
removeObserver: forKeyPath: context:
方法移除觀察者。
3.1 觀察單個屬性
進(jìn)入ViewController.m
,在實現(xiàn)部分添加viewWillAppear:
方法,在viewWillAppear:
方法內(nèi)添加firstName
和age
屬性為觀察對象。
- (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:
注冊成為觀察者,以便接收KVO通知,通常為self
,該對象必須實現(xiàn)observerValueForKeyPath: ofObject: change: context:
以接收屬性變化通知。 -
keyPath:
要觀察的屬性字符串,必須和屬性一致,不能為空。 -
options:
用來指定通知詞典中應(yīng)包含值類型。如果參數(shù)是NSKeyValueObservingOptionNew
,詞典包含新產(chǎn)生的值;如果參數(shù)是NSKeyValueObservingOptionOld
,詞典包含變化前的值;如果參數(shù)是數(shù)字0
,詞典不包括任何值;如果需要change
詞典同時包括新產(chǎn)生值和變化前的舊值,可以像上面代碼一樣使用|
,即或運算符;任何時候都可以使用[object valueForKey:<Key>]
方法獲取屬性變化產(chǎn)生的新值。 -
context:
這是一個指針,可用做我們觀察到的屬性更改的唯一標(biāo)志符,經(jīng)常設(shè)置為NULL
,后面會詳細(xì)說明。
現(xiàn)在我們已經(jīng)可以觀察到firstName
和age
兩個屬性的的變化,KVO觀察到每一個觀察對象的變化都會調(diào)用observerValueForKeyPath: ofObject: change: context:
方法,如果觀察多個屬性的變化,觀察方法內(nèi)if
語句可能很長,下面實現(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
判斷哪一個屬性改變了,之后輸出。在輸出時,可以直接輸出詞典change
,也可以用valueForKey:
獲取詞典中的值,此處的鍵用NSKeyValueChangeNewKey
或NSKeyValueChangeOldKey
,前者獲取新產(chǎn)生的值,后者獲取改變前的舊值。現(xiàn)在在viewwillAppear:
底部添加下面代碼來驗證是否可以觀察到屬性變化。
- (void)viewWillAppear:(BOOL)animated
{
...
// 添加觀察者后改變值 驗證是否可以觀察到值變化
[self.child1 setValue:@"Tiffany" forKey:@"firstName"];
[self.child1 setValue:[NSNumber numberWithUnsignedInteger:23] forKey:@"age"];
}
運行,輸出內(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觀察屬性變化變的如此簡單。現(xiàn)在在viewWillAppear:
方法中添加觀察者,觀察child2
對象的屬性變化,隨后為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"];
}
運行后輸出如下:
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
正如看到的一樣,我們收到兩個age
屬性的改變通知,盡管我們自己可以區(qū)分出每一個通知來自哪一個對象屬性的改變,但在程序中,目前我們無法對此進(jìn)行區(qū)分。為解決這個問題,我們將使用addObserver: forKeyPath: options: context:
方法中的context
參數(shù)。context
參數(shù)一般使用下面聲明方法。
static void *XXContext = &XXContext;
表示一個靜態(tài)變量存放著它自己的指針,也就是它自己什么也沒有。因為要在addObserver: forKeyPath: options: context:
和observerValueForKeyPath: ofObject: change: context:
兩個方法中使用context
,這里的context
聲明為靜態(tài)全局變量。
在ViewController.m
實現(xiàn)前添加下面兩個聲明。
@end
static void *child1Context = &child1Context;
static void *child2Context = &child2Context;
@implementation ViewController
你也可以把context
聲明為屬性,但聲明為全局變量更為簡單。現(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];
// 添加觀察者后改變值 驗證是否可以觀察到值變化
[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ū)分出通知來自哪一個對象屬性的變化。
- (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 注冊相互影響的鍵
在許多情況下,一個屬性的值取決另一個對象中的一個或多個其他屬性的值。如果一個屬性的值改變,那么派生屬性的值也應(yīng)該改變。例如:姓名由姓和名兩個屬性組成,當(dāng)其中任何一個屬性變化時,姓名屬性都要得到改變的通知。
進(jìn)入Children.h
,添加NSString
類型的fullName
屬性和lastName
兩個屬性。
@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
屬性變化時,必須通知fullName
屬性的應(yīng)用程序,因為它們會影響fullName
屬性的值。可以通過實現(xiàn)類方法keyPathsForValuesAffecting<key>
來獲取哪些屬性會影響<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
屬性變化時進(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)在你可以運行下,可以輸出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ù)組的變化需要做一些額外的工作。事實上,有很多關(guān)于數(shù)組的細(xì)節(jié),但在這里,我們只講解一些基礎(chǔ)的、重要的內(nèi)容。因為數(shù)組不符合KVC,因此觀察數(shù)組不像觀察上面示例中的屬性那么簡單。我們要實現(xiàn)一些關(guān)于數(shù)組的方法以便使數(shù)組符合KVC,進(jìn)而可以使用KVO觀察數(shù)組的變化。
這里我們將討論可變數(shù)組,不可變數(shù)組與可變數(shù)組類似,只是需要實現(xiàn)的方法少一些。假設(shè)我們有一個可變數(shù)組myArray
,這里需要實現(xiàn)的方法與數(shù)組的插入、移除、計數(shù)類似,不同之處在于數(shù)組的名稱,需要實現(xiàn)方法如下:
- countOfMyArray
- objectInMyArrayAtIndex:
- insertObject:inMyArrayAtIndex:
- removeObjectFromMyArrayAtIndex:
這些方法都很熟悉,不同的是我們用數(shù)組的名稱替換里面名稱。如果是不可變數(shù)組,只需要取消實現(xiàn)最后兩個方法。
讓數(shù)組符合KVC有好的一方面,也有不利的一方面。好處是Xcode會對數(shù)組名建議補全;壞的一方面是類中每一個想使用KVC觀察的數(shù)組都要實現(xiàn)這些方法,會產(chǎn)生大量代碼。為了避免產(chǎn)生大量重復(fù)代碼,我們可以創(chuàng)建一個新的類,類內(nèi)只包含一個可變數(shù)組,在這個數(shù)組內(nèi)實現(xiàn)這些方法讓這個數(shù)組符合KVC,這樣在其他類中使用這個類的實例對象。這樣的好處是:讓數(shù)組符合KVC,必須實現(xiàn)的方法只需要實現(xiàn)一次,這個類可以重復(fù)使用。你可以理解為這是一個高級版本的數(shù)組。
現(xiàn)在添加一個新類,點擊File > New > File…,選取iOS > Source > Cocoa Touch Class模板,點擊Next;類名稱為KVCMutableArray
,父類為NSObject
,點擊Next;選擇文件位置,點擊Create創(chuàng)建文件。
進(jìn)入KVCMutableArray.h
文件,聲明一個可變數(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
對象這里設(shè)定為id
類型,以便其他類可以使用。進(jìn)入KVCMutableArray.m
,添加init
方法,初始化array
,實現(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)建了一個符合KVC的數(shù)組。
現(xiàn)在我們要在Children.h
中添加一個可變數(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
中初始化剛聲明的對象。
- (instancetype)init
{
self = [super init];
if (self)
{
...
_cousins = [KVCMutableArray new];
}
return self;
}
進(jìn)入ViewController.m
文件,在viewWillAppear:
方法底部添加如下代碼:
- (void)viewWillAppear:(BOOL)animated
{
...
// 對數(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)父類也在觀察屬性時會有幫助。最后,如果沒有合適的context
或keyPath
,把它交給父類來處理,可能是父類也在觀察同一個屬性。
運行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 手動發(fā)送通知
默認(rèn)情況下,KVO觀察到屬性變化系統(tǒng)會自動發(fā)送通知,但在某些情況下,你可能需要控制何時發(fā)送通知。例如:在某些情況下不需要發(fā)送通知,或?qū)⒍鄠€改變合并為一個通知發(fā)送。手動發(fā)送通知提供了執(zhí)行此操作的方法。
手動和自動通知并不互斥,已經(jīng)存在自動通知的類內(nèi)也可以添加手動通知。你可以通過重寫由NSObject
實現(xiàn)的automaticallyNotifiesObserversForKey:
類方法來控制特定屬性的通知發(fā)送,這個方法的參數(shù)key
就是想要手動控制通知的屬性,這個方法返回值類型是BOOL
類型,想要手動控制通知的屬性在重寫這個類方法時返回NO
,其他屬性由超類來處理。
假設(shè)我們現(xiàn)在不想接收firstName
屬性的變化,進(jìn)入Children.m
文件,在實現(xiàn)部分添加下面類方法。
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
BOOL automatic = NO;
if ([key isEqualToString:@"firstName"])
{
automatic = NO;
} else {
automatic = [super automaticallyNotifiesObserversForKey:key];
}
return automatic;
}
上面的方法非常簡單,暫停firstName
屬性的自動通知;在else
部分,使用超類調(diào)用相同方法,以便讓iOS處理所有未在上面顯式添加的屬性。
現(xiàn)在運行app,你會發(fā)現(xiàn)所有firstName
屬性的變化都沒有輸出。現(xiàn)在只是能夠停止特定鍵對應(yīng)屬性變化的通知,還不能稱為手動發(fā)送通知。
想要手動發(fā)送通知,需要添加willChangeValueForKey:
和didChangeValueForKey:
方法。步驟如下:
- 調(diào)用
willChangeValueForKey:
方法。 - 修改所觀察屬性的值。
- 調(diào)用
didChangeValueForKey:
方法。
進(jìn)入ViewController.m
文件,在viewWillAppear:
方法中找到[self.child1 setValue:@"Tiffany" forKey:@"firstName"];
這一行代碼,并用下面三行代碼替換。
- (void)viewWillAppear:(BOOL)animated
{
...
// 添加觀察者后改變值 驗證是否可以觀察到值變化
[self.child1 willChangeValueForKey:@"firstName"];
[self.child1 setValue:@"Tiffany" forKey:@"firstName"];
[self.child1 didChangeValueForKey:@"firstName"];
...
}
現(xiàn)在運行app,firstName
屬性的改變會在控制臺輸出,也就是我們已經(jīng)成功手動發(fā)送通知。
The name of the FIRST child was changed.
{
kind = 1;
new = Tiffany;
old = Jr;
}
事實上,通知是在調(diào)用didChangeValueForKey:
方法后發(fā)送的。如果不想在改變屬性值后立即發(fā)送通知,可以在改變屬性后任何想要發(fā)送通知的位置調(diào)用didChangeValueForKey:
方法。例如這個demo中,你可以把[self.child1 didChangeValueForKey:@"firstName"];
放在程序行代碼的最底部,控制臺內(nèi)容輸出順序?qū)l(fā)生變化,這里不再詳細(xì)說明。
如果單個操作導(dǎo)致多個鍵改變,則必須嵌套更改通知。如下:
[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"];
可以把多個手動通知嵌套在一起,每個手動通知只能觀察到鍵最新一次的改變。如上面代碼,只有2可以觀察到改變,1的改變不能觀察到。
最后一定要記得移除觀察者。如果視圖控制器釋放前沒有移除觀察者,釋放時app會崩潰。一般添加觀察者在viewDidLoad
方法、viewWillAppear:
中,移除觀察者可以在observerValueForKeyPath: ofObject: change: context:
處理完通知后,或viewWillDisappear:
方法中,也可以在dealloc
方法中。在這個demo中,我們在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];
}
每一個addObserver: forKeyPath: options: context:
必須對應(yīng)一個removeObserver: forKeyPath: context:
,KVO沒有辦法判斷當(dāng)前控制器是否被注冊為觀察者,并且移除不存在的觀察者,app也會崩潰。
總結(jié)
鍵值觀察提供了一種允許對象在其他類屬性變化時獲得通知的機制。對于應(yīng)用程序中模型層和控制器層通信特別有用。控制器對象通常用來觀察模型對象的屬性,并且視圖對象也可以通過控制器對象觀察模型對象的屬性。此外,模型對象可以觀察其他模型對象,也可以觀察自身。
鍵值觀察和鍵值編碼都是一種幫助建立更強大、更靈活、更高效的應(yīng)用,可能剛接觸時覺得很奇特,最后你會感覺這些很容易掌握。
這篇文章只介紹了KVC、KVO的用法,如果你想要了解KVC、KVO的底層原理,請查看我的另一篇文章:KVC、KVO的本質(zhì)。
文件名稱:KVC&KVODemo
源碼地址:https://github.com/pro648/BasicDemos-iOS
參考資料: