iOS-KVO(一) 基本操作

iOS-KVO(一) 基本操作
iOS-KVO(二) 使用注意點
iOS-KVO(三) 窺探底層實現(xiàn)
iOS-KVO(四) 自定義KVO+Block

KVO(key-value-observing)是實現(xiàn)鍵值觀察的方式,當某個屬性的值發(fā)生變化的時候,通知觀察者;
KVO本質(zhì)上其實是一個觀察者模式;
一般繼承自NSObject的對象都默認支持KVO;

(1)使用KVO的三個步驟

  1. 注冊O(shè)bserver;
  2. 接收屬性值的變化;
  3. 移除Observer;

先介紹以下每個步驟對應(yīng)的方法,以及對應(yīng)的參數(shù)的含義

注冊O(shè)bserver
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;

對應(yīng)的參數(shù):
observer:觀察者,監(jiān)聽屬性變化的對象,該對象必須實現(xiàn)observeValueForKeyPath:ofObject:change:context:方法;
keyPath:要觀察的屬性名稱;
options:調(diào)用接收方法的時機以及包含的內(nèi)容;
context:上下文,可以傳入任意類型的對象,將在消息回調(diào)的時,可以接收到這個對象,是KVO的一種傳值方式;
其中 options參數(shù):

     typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
        NSKeyValueObservingOptionNew = 0x01,
        NSKeyValueObservingOptionOld = 0x02,
        NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
        NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
     };

     NSKeyValueObservingOptionNew:接收方法的change參數(shù)的中包含新的值(NSKeyValueChangeNewKey)
     NSKeyValueObservingOptionOld:接收方法的change參數(shù)的中包含舊的值(NSKeyValueChangeOldKey)
     NSKeyValueObservingOptionInitial:注冊的時候發(fā)一次通知,改變后也發(fā)送一次通知
     NSKeyValueObservingOptionPrior:屬性改變之前發(fā)一次,改變之后再發(fā)一次,變化前的通知change參數(shù)包含notificationIsPrior = 1

接收方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context;

對應(yīng)的參數(shù):
keyPath:被觀察的屬性
object:被觀察的對象
change:在注冊時用options參數(shù)進行的配置,會包含不同的內(nèi)容
context:注冊的上下文

其中change的key(NSKeyValueChangeKey)有以下幾種

  • NSKeyValueChangeKindKey:對應(yīng)著四個修改的類型,其中后面三個是集合對象的操作方式
    NSKeyValueChangeSetting = 1, 賦值
    NSKeyValueChangeInsertion = 2, 插入
    NSKeyValueChangeRemoval = 3, 移除
    NSKeyValueChangeReplacement = 4, 替換

  • NSKeyValueChangeNewKey:新值

  • NSKeyValueChangeOldKey:舊值

  • NSKeyValueChangeIndexesKey

  • NSKeyValueChangeNotificationIsPriorKey

NSKeyValueChangeKindKey.png
移除觀察者
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;

對應(yīng)的參數(shù):
observer:要移除的觀察者;
keyPath:要移除的屬性名稱;
注意:一定要在合適的機會移除觀察者,否則會引發(fā)泄露,我將會在下一篇中列舉出。

(2)觸發(fā)接收方法的幾種情況

  1. 直接調(diào)用setter方法,或者通過屬性的點語法間接調(diào)用;
  2. 使用KVC的setValue:forKey: 或者 setValue:forKeyPath:方法;
  3. 通過mutableArrayValueForKey:K方法獲取到數(shù)組代理對象,并使用代理對象進行操作;
    注意:直接給成員賦值是不會觸發(fā)的

(3)使用案例

案例一:普通操作

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSNumber *PID;  //人ID
@property (nonatomic, copy) NSString *name;  //名字
@property (nonatomic, copy) NSString *address;  //地址

@end

NS_ASSUME_NONNULL_END
#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *p = [[Person alloc] init];
    self.person = p;
    
    [p addObserver:self forKeyPath:@"PID" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //點擊屏幕,設(shè)置person的PID屬性值
    static NSInteger count = 1;
    count++;
    self.person.PID = @(count);
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"PID"];
    self.person = nil;
    NSLog(@"%@ dealloc", [self class]);
}


@end

點擊屏幕輸出結(jié)果:

2019-07-02 21:58:12.528122+0800 KVODemo[8046:234497] {
    kind = 1;
    new = 2;
    old = "<null>";
}

案例二:觀察Person對象中的Info對象的name屬性

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Info : NSObject

@property (nonatomic, copy) NSString *name;  //名字
@property (nonatomic, copy) NSString *address;  //地址

@end

NS_ASSUME_NONNULL_END
#import <Foundation/Foundation.h>
#import "Info.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property (nonatomic, copy) NSNumber *PID;  //人ID
@property (nonatomic, strong) Info *info; //個人信息

@end

NS_ASSUME_NONNULL_END

#import "Person.h"

@implementation Person

- (instancetype)init
{
    self = [super init];
    
    if ( self ) {
        _info = [Info new];
    }
    
    return self;
}

@end

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong) Person *person;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *p = [[Person alloc] init];
    self.person = p;
    //觀察person的info的name屬性
    [self.person addObserver:self forKeyPath:@"info.name" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
    
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@", change);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //點擊屏幕,設(shè)置person的info的name屬性值
    self.person.info.name = @"hui";
}

- (void)dealloc
{
    [self.person removeObserver:self forKeyPath:@"info.name"];
    self.person = nil;
    NSLog(@"%@ dealloc", [self class]);
}


@end

點擊屏幕輸出結(jié)果:

2019-07-02 22:40:45.128484+0800 KVODemo[8667:250652] {
    kind = 1;
    new = hui;
    old = "<null>";
}

案例三:觀察Person對象中的Info對象多個屬性
代碼就不全部貼了,只貼重要代碼;
主要的是在被觀察者的對象中重寫

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key;

根據(jù)自己的需求,添加觀察的哪些屬性。如我想觀察info對象中的name和address的屬性。
那么代碼如下:

+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    
    if ( [key isEqualToString:@"info"] ) {
        
        keyPaths = [keyPaths setByAddingObjectsFromArray:@[@"_info.name", @"_info.address"]];
        
    }
    
    return keyPaths;
}

觀察的keyPaths為info。

    [self.person addObserver:self forKeyPath:@"info" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];

當info對象的name或者address屬性變化時候,將會自動調(diào)用接收方法。

案例四:手動觸發(fā)
注冊后,KVO默認會自動通知觀察者。其實我們可以手動觸發(fā),在滿足某些條件我們在觸發(fā)調(diào)用接收方法。

如果你想取消自動通知,主動在被觀察者類中實現(xiàn)下面方法:

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key;
返回值:
  YES:主動觸發(fā);
  NO:自動觸發(fā);

針對非自動通知的屬性,可以分別在變化之前和之后手動調(diào)用如下方法(will在前,did在后)來手動通知觀察者:

-(will/did)ChangeValueForKey:
-(will/did)ChangeValueForKey:withSetMutation:usingObjects:
-(will/did)Change:valuesAtIndexes:forKey:

實際上自動通知也是框架通過調(diào)用這些方法實現(xiàn)的。
直接用案例一的代碼,不全部列出來了。只列舉重要的部分。
觀察Person對象的name屬性,不讓其主動觸發(fā)。在Person類中加入

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ( [key isEqualToString:@"name"] ) {
        return NO;
    }
    return YES;
}

觸摸屏幕時主動觸發(fā)接收方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //點擊屏幕,設(shè)置person的info的name屬性值
    [self willChangeValueForKey:@"name"];
    self.person.name = @"hui";
    [self didChangeValueForKey:@"name"];
}

over!

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

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

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