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

在編程中,最常見的就是程序的流程取決于你所使用的各種變量和屬性的值猪贪,根據(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í)我們只需要將值與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í)間了。

打開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的步驟:

  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:用來指定通知詞典中應(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)可以觀察到firstNameage兩個(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:獲取詞典中的值,此處的鍵用NSKeyValueChangeNewKeyNSKeyValueChangeOldKey棠耕,前者獲取新產(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,初始化剛聲明的屬性衡怀,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屬性的值∮窆蓿可以通過實(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屬性狰闪,之后修改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ì)有幫助办陷。最后,如果沒有合適的contextkeyPath民镜,把它交給父類來處理制圈,可能是父類也在觀察同一個(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:方法迹炼。步驟如下:

  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ì)說明疙筹。

如果單個(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

參考資料:

  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)系作者
  • 序言:七十年代末摩渺,一起剝皮案震驚了整個(gè)濱河市简烤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摇幻,老刑警劉巖横侦,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異绰姻,居然都是意外死亡枉侧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門狂芋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來榨馁,“玉大人,你說我怎么就攤上這事帜矾∫沓妫” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵屡萤,是天一觀的道長(zhǎng)珍剑。 經(jīng)常有香客問我,道長(zhǎng)死陆,這世上最難降的妖魔是什么招拙? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮翔曲,結(jié)果婚禮上迫像,老公的妹妹穿的比我還像新娘劈愚。我一直安慰自己瞳遍,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布菌羽。 她就那樣靜靜地躺著掠械,像睡著了一般。 火紅的嫁衣襯著肌膚如雪注祖。 梳的紋絲不亂的頭發(fā)上猾蒂,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音是晨,去河邊找鬼肚菠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛罩缴,可吹牛的內(nèi)容都是我干的蚊逢。 我是一名探鬼主播层扶,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼烙荷!你這毒婦竟也來了镜会?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤终抽,失蹤者是張志新(化名)和其女友劉穎戳表,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昼伴,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡匾旭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了圃郊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片季率。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖描沟,靈堂內(nèi)的尸體忽然破棺而出飒泻,到底是詐尸還是另有隱情,我是刑警寧澤吏廉,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布泞遗,位于F島的核電站,受9級(jí)特大地震影響席覆,放射性物質(zhì)發(fā)生泄漏史辙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一佩伤、第九天 我趴在偏房一處隱蔽的房頂上張望聊倔。 院中可真熱鬧,春花似錦生巡、人聲如沸耙蔑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)甸陌。三九已至,卻和暖如春盐股,著一層夾襖步出監(jiān)牢的瞬間钱豁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工疯汁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牲尺,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓幌蚊,卻偏偏與公主長(zhǎng)得像谤碳,于是被迫代替她去往敵國(guó)和親凛澎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

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