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

在編程中,最常見的就是程序的流程取決于你所使用的各種變量和屬性的值,根據(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時我們只需要將值與KeyKeyPath匹配就可以,使用字符串間接把值賦給屬性。如果需要獲取屬性的值,可用下面方式:

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

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

3.1 觀察單個屬性

進(jìn)入ViewController.m,在實現(xiàn)部分添加viewWillAppear:方法,在viewWillAppear:方法內(nèi)添加firstNameage屬性為觀察對象。

- (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)可以觀察到firstNameage兩個屬性的的變化,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:獲取詞典中的值,此處的鍵用NSKeyValueChangeNewKeyNSKeyValueChangeOldKey,前者獲取新產(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,初始化剛聲明的屬性,fullNamefirstNamelastName組成。

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

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

當(dāng)firstNamelastName屬性變化時,必須通知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屬性,之后修改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屬性變化時進(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)父類也在觀察屬性時會有幫助。最后,如果沒有合適的contextkeyPath,把它交給父類來處理,可能是父類也在觀察同一個屬性。

運行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:方法。步驟如下:

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

參考資料:

  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)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,967評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,273評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,870評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,742評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,527評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,010評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,108評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,250評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,769評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,656評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,853評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,371評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,103評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,472評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,717評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,487評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,815評論 2 372

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

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