KVC和KVO學(xué)習(xí)筆記

在編程中,最常見(jiàn)的就是程序的流程取決于你所使用的各種變量和屬性的值,根據(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ǔ)上,也就是說(shuō)任何你想使用KVO觀察的屬性必須符合鍵值編碼。

KVC和KVO提供了一個(gè)強(qiáng)大高效的方式來(lái)編寫代碼,學(xué)習(xí)KVO前必須先掌握KVC,所以下面我們結(jié)合demo來(lái)學(xué)習(xí)KVC。在這個(gè)demo中所有結(jié)果將直接在控制臺(tái)輸出,沒(mé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替代訪問(wèn)器方法設(shè)值。使用KVC時(shí)我們只需要將值與KeyKeyPath匹配就可以,使用字符串間接把值賦給屬性。如果需要獲取屬性的值,可用下面方式:

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í)間了。

打開(kāi)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)在開(kāi)始學(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)語(yǔ)法的字符串@"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字符串以@開(kāi)頭時(shí)會(huì)遇到問(wèn)題。所以在詞典中使用objectForKey:,在KVC中使用valueForKey:valueForKeyPath:。

3. 鍵值觀察

我們已經(jīng)掌握了KVC,現(xiàn)在開(kāi)始學(xué)習(xí)KVO。以下是實(shí)現(xiàn)KVO的步驟:

  1. 使用addObserver: forKeyPath: options: context:方法注冊(cè)為觀察者,用于觀察其他類的屬性。
  2. 觀察者必須實(shí)現(xiàn)observerValueForKeyPath: ofObject: change: context:方法以接收屬性變化通知。
  3. 使用removeObserver: forKeyPath: context:方法移除觀察者。

3.1 觀察單個(gè)屬性

進(jìn)入ViewController.m,在實(shí)現(xiàn)部分添加viewWillAppear:方法,在viewWillAppear:方法內(nèi)添加firstNameage屬性為觀察對(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:用來(lái)指定通知詞典中應(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ì)說(shuō)明。

現(xiàn)在我們已經(jīng)可以觀察到firstNameage兩個(gè)屬性的的變化,KVO觀察到每一個(gè)觀察對(duì)象的變化都會(huì)調(diào)用observerValueForKeyPath: ofObject: change: context:方法,如果觀察多個(gè)屬性的變化,觀察方法內(nèi)if語(yǔ)句可能很長(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:獲取詞典中的值,此處的鍵用NSKeyValueChangeNewKeyNSKeyValueChangeOldKey,前者獲取新產(chǎn)生的值,后者獲取改變前的舊值?,F(xiàn)在在viewwillAppear:底部添加下面代碼來(lái)驗(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)單?,F(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è)通知來(lái)自哪一個(gè)對(duì)象屬性的改變,但在程序中,目前我們無(wú)法對(duì)此進(jìn)行區(qū)分。為解決這個(gè)問(wèn)題,我們將使用addObserver: forKeyPath: options: context:方法中的context參數(shù)。context參數(shù)一般使用下面聲明方法。

static void *XXContext = &XXContext;

表示一個(gè)靜態(tài)變量存放著它自己的指針,也就是它自己什么也沒(mé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)單?,F(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ū)分出通知來(lái)自哪一個(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,初始化剛聲明的屬性,fullNamefirstNamelastName組成。

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        ...
        _lastName = @"";
    }
    
    return self;
}

- (NSString *)fullName
{
    return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}

當(dāng)firstNamelastName屬性變化時(shí),必須通知fullName屬性的應(yīng)用程序,因?yàn)樗鼈儠?huì)影響fullName屬性的值??梢酝ㄟ^(guò)實(shí)現(xiàn)類方法keyPathsForValuesAffecting<key>來(lái)獲取哪些屬性會(huì)影響<key>屬性,這里的<key>fullName,首字母要大寫。在Children.m中添加以下類方法:

+ (NSSet *)keyPathsForValuesAffectingFullName
{
    return [NSSet setWithObjects:@"firstName",@"lastName", nil];
}

進(jìn)入ViewController.m文件,在viewWillAppear:方法中添加觀察者,觀察fullname屬性,之后修改lastNamefirstName屬性。

- (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ì)有幫助。最后,如果沒(méi)有合適的contextkeyPath,把它交給父類來(lái)處理,可能是父類也在觀察同一個(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)通知。你可以通過(guò)重寫由NSObject實(shí)現(xiàn)的automaticallyNotifiesObserversForKey:類方法來(lái)控制特定屬性的通知發(fā)送,這個(gè)方法的參數(shù)key就是想要手動(dòng)控制通知的屬性,這個(gè)方法返回值類型是BOOL類型,想要手動(dòng)控制通知的屬性在重寫這個(gè)類方法時(shí)返回NO,其他屬性由超類來(lái)處理。

假設(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屬性的變化都沒(méi)有輸出?,F(xiàn)在只是能夠停止特定鍵對(duì)應(yīng)屬性變化的通知,還不能稱為手動(dòng)發(fā)送通知。

想要手動(dòng)發(fā)送通知,需要添加willChangeValueForKey:didChangeValueForKey:方法。步驟如下:

  1. 調(diào)用willChangeValueForKey:方法。
  2. 修改所觀察屬性的值。
  3. 調(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ì)說(shuō)明。

如果單個(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的改變不能觀察到。

最后一定要記得移除觀察者。如果視圖控制器釋放前沒(méi)有移除觀察者,釋放時(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沒(méi)有辦法判斷當(dāng)前控制器是否被注冊(cè)為觀察者,并且移除不存在的觀察者,app也會(huì)崩潰。

總結(jié)

鍵值觀察提供了一種允許對(duì)象在其他類屬性變化時(shí)獲得通知的機(jī)制。對(duì)于應(yīng)用程序中模型層和控制器層通信特別有用??刂破鲗?duì)象通常用來(lái)觀察模型對(duì)象的屬性,并且視圖對(duì)象也可以通過(guò)控制器對(duì)象觀察模型對(duì)象的屬性。此外,模型對(duì)象可以觀察其他模型對(duì)象,也可以觀察自身。

鍵值觀察和鍵值編碼都是一種幫助建立更強(qiáng)大、更靈活、更高效的應(yīng)用,可能剛接觸時(shí)覺(jué)得很奇特,最后你會(huì)感覺(jué)這些很容易掌握。

這篇文章只介紹了KVC、KVO的用法,如果你想要了解KVC、KVO的底層原理,請(qǐng)查看我的另一篇文章:KVC、KVO的本質(zhì)。

文件名稱:KVC&KVODemo
源碼地址:https://github.com/pro648/BasicDemos-iOS

參考資料:

  1. Key-Value Observing Programming Guide
  2. Understanding Key-Value Observing and Coding
  3. KVO Considered Harmful

歡迎更多指正:https://github.com/pro648/tips/wiki

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載:http://yulingtianxia.com/blog/2014/05/12/objective-czh...
    F麥子閱讀 1,085評(píng)論 0 0
  • 本文講述了使用Cocoa框架中的KVC和KVO,實(shí)現(xiàn)觀察者模式 鍵/值編碼中的基本調(diào)用包括-valueForKey...
    茗涙閱讀 758評(píng)論 0 3
  • 本文結(jié)構(gòu)如下: Why? (為什么要用KVO) What? (KVO是什么) How? ( KVO怎么用) Mo...
    等開(kāi)會(huì)閱讀 1,728評(píng)論 1 21
  • 本文轉(zhuǎn)自:Objective-C中的KVC和KVO. KVC KVO2.1. Registering for Ke...
    0o凍僵的企鵝o0閱讀 491評(píng)論 0 3
  • 本文由我們團(tuán)隊(duì)的 糾結(jié)倫 童鞋撰寫。 文章結(jié)構(gòu)如下: Why? (為什么要用KVO) What? (KVO是什么...
    知識(shí)小集閱讀 7,484評(píng)論 7 105

友情鏈接更多精彩內(nèi)容