使用Cocoa框架中的KVC和KVO

2017-03-24  本文已影响45人  茗涙

本文讲述了使用Cocoa框架中的KVC和KVO,实现观察者模式

键/值编码中的基本调用包括-valueForKey:和-setValue:forKey:
。以字符串的形式向对象发送消息,这个字符串是我们关注的属性的关键。

最简单的 KVC 能让我们通过以下的形式访问属性:

@property (nonatomic, copy) NSString *name;

取值:

NSString *n = [object valueForKey:@"name"];

设定:

[object setValue:@"Daniel" forKey:@"name"];

举例来说,如果有以下属性:

@property (nonatomic) CGFloat height;

我们可以这样设置它:

[object setValue:@(20) forKey:@"height"];

有关KVC的更多用法,参看下面的文章:
http://blog.csdn.net/omegayy/article/details/7381301
http://blog.csdn.net/omegayy/article/details/7381301
http://blog.csdn.net/wzzvictory/article/details/9674431
http://blog.csdn.net/wzzvictory/article/details/9674431
http://objccn.io/issue-7-3/](http://objccn.io/issue-7-3/
http://yulingtianxia.com/blog/2014/05/12/objective-czhong-de-kvche-kvo/#KVO)KVO

比如,BankObject中的accountBalance属性有任何变更时,某个PersonObject对象都要觉察到。这个PersonObject对象必须注册成为BankObject的accountBalance属性的观察者,可以通过发送

addObserver:forKeyPath:options:context:

消息来实现。

注意:

addObserver:forKeyPath:options:context:

方法在你指定的两个实例间建立联系,而不是在两个类之间。
为了回应变更通知,观察者必须实现

observeValueForKeyPath:ofObject:change:context:

方法。这个方法的实现决定了观察者如何回应变更通知。你可以在这个方法里自定义如何回应被观察属性的变更。

当一个被观察属性的值以符合KVO方式变更或者当它依赖的键变更时,

observeValueForKeyPath:ofObject:change:context:

方法会被自动执行。


Registering for Key-Value Observing

注册成为观察者
你可以通过发送

addObserver:forKeyPath:options:context:

消息来注册观察者


- (void)registerAsObserver 
{ 
/* Register 'inspector' to receive change notifications for the
 "openingBalance" property of the 'account' object and specify
 that both the old and new values of "openingBalance" should be provided in the observe… method. */ 
[account addObserver:inspector forKeyPath:@"openingBalance" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
}

inspector注册成为了account的观察者,被观察属性的KeyPath是@"openingBalance",也就是account的openingBalance属性,NSKeyValueObservingOptionNew和NSKeyValueObservingOptionOld选项分别标识在观察者接收通知时change字典对应入口提供更改后的值和更改前的值。更简单的办法是用 NSKeyValueObservingOptionPrior选项,随后我们就可以用以下方式提取出改变前后的值:

change是个字典,详细介绍请看下

id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];
observeValueForKeyPath:ofObject:change:context:

方法执行时context会提供给观察者。context可以是C指针或者一个对象引用,既可以当作一个唯一的标识来分辨被观察的变更,也可以向观察者提供数据。

observeValueForKeyPath:ofObject:change:context:

消息,所有的观察者都必须实现这个方法。观察者会被提供触发通知的对象和keyPath,一个包含变更详细信息的字典,还有一个注册观察者时提供的context指针。

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqual:@"openingBalance"]) {
        [openingBalanceInspectorField setObjectValue: [change objectForKey:NSKeyValueChangeNewKey]]; }
    /* Be sure to call the superclass's implementation *if it implements it*. NSObject does not implement the method. */
    [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

关于change参数,它是一个字典,有五个常量作为它的键:

NSString *const NSKeyValueChangeKindKey; 
NSString *const NSKeyValueChangeNewKey;
NSString *const NSKeyValueChangeOldKey;
NSString *const NSKeyValueChangeIndexesKey; 
NSString *const NSKeyValueChangeNotificationIsPriorKey;

NSKeyValueChangeKindKey指明了变更的类型,值为“NSKeyValueChange”枚举中的某一个,类型为NSNumber。

enum {
   NSKeyValueChangeSetting = 1,
   NSKeyValueChangeInsertion = 2,
   NSKeyValueChangeRemoval = 3,
   NSKeyValueChangeReplacement = 4
};
typedef NSUInteger NSKeyValueChange;
if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
    // 改变之前
} else {
    // 改变之后
}

移除观察者

你可以通过发送

removeObserver:forKeyPath:

消息来移除观察者,你需要指明观察对象和路径。

- (void)unregisterForChangeNotification {
    [observedObject removeObserver:inspector forKeyPath:@"openingBalance"];
}

上面的代码将openingBalance
属性的观察者inspector移除,移除后观察者再也不会收到observeValueForKeyPath:ofObject:change:context:消息。在移除观察者之前,如果context是一个对象的引用,那么必须保持对它的强引用直到观察者被移除

KVO Compliance(KVO兼容)

有两种方法可以保证变更通知被发出。自动发送通知是NSObject
提供的,并且一个类中的所有属性都默认支持,只要是符合KVO的。一般情况你使用自动变更通知,你不需要写任何代码。人工变更通知需要些额外的代码,但也对通知发送提供了额外的控制。你可以通过重写子类automaticallyNotifiesObserversForKey:
方法的方式控制子类一些属性的自动通知。

Automatic Change Notification(自动通知)

下面代码中的方法都能导致KVO变更消息发出

// Call the accessor method.
[account setName:@"Savings"];
 
// Use setValue:forKey:.
[account setValue:@"Savings" forKey:@"name"];
 
// Use a key path, where 'account' is a kvc-compliant property of 'document'.
[document setValue:@"Savings" forKeyPath:@"account.name"];
 
// Use mutableArrayValueForKey: to retrieve a relationship proxy object.
Transaction *newTransaction = <#Create a new transaction for the account#>;
NSMutableArray *transactions = [account mutableArrayValueForKey:@"transactions"];
[transactions addObject:newTransaction];

Manual Change Notification(手动通知)

下面的代码为openingBalance
属性开启了人工通知,并让父类决定其他属性的通知方式。

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
 
    BOOL automatic = NO;
    if ([theKey isEqualToString:@"openingBalance"]) {
        automatic = NO;
    }
    else {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}

要实现人工观察者通知,你要执行在变更前执行

willChangeValueForKey:

方法,在变更后执行

didChangeValueForKey:

方法:

- (void)setOpeningBalance:(double)theBalance {
    [self willChangeValueForKey:@"openingBalance"];
    _openingBalance = theBalance;
    [self didChangeValueForKey:@"openingBalance"];
}

为了使不必要的通知最小化我们应该在变更前先检查一下值是否变了:

- (void)setOpeningBalance:(double)theBalance {
    if (theBalance != _openingBalance) {
        [self willChangeValueForKey:@"openingBalance"];
        _openingBalance = theBalance;
        [self didChangeValueForKey:@"openingBalance"];
    }
}

如果一个操作导致了多个键的变化,你必须嵌套变更通知:

- (void)setOpeningBalance:(double)theBalance {
    [self willChangeValueForKey:@"openingBalance"];
    [self willChangeValueForKey:@"itemChanged"];
    _openingBalance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"openingBalance"];
}

在to-many关系操作的情形中,你不仅必须表明key是什么,还要表明变更类型和影响到的索引。变更类型是一个 NSKeyValueChange
值,被影响对象的索引是一个 NSIndexSet
对象。下面的代码示范了在to-many关系transactions
对象中的删除操作:

- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
    [self willChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
 
    // Remove the transaction objects at the specified indexes.
 
    [self didChange:NSKeyValueChangeRemoval
        valuesAtIndexes:indexes forKey:@"transactions"];
}

Registering Dependent Keys(注册依赖键)

有一些属性的值取决于一个或者多个其他对象的属性值,一旦某个被依赖的属性值变了,依赖它的属性的变化也需要被通知。

To-one Relationships

要自动触发 to-one关系,有两种方法:重写

keyPathsForValuesAffectingValueForKey:

方法或者定义名称为keyPathsForValuesAffecting<Key>
的方法。
例如一个人的全名是由姓氏和名子组成的:

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

一个观察fullName的程序在firstName或者lastName
变化时也应该接收到通知。

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
 
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
 
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"lastName", @"firstName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

相当于在影响fullName值的keypath中新加了两个key:lastName和firstName,很容易理解。

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

有时在类别中我们不能添加keyPathsForValuesAffectingValueForKey:
方法,因为不能再类别中重写方法,所以这时可以实现keyPathsForValuesAffecting<Key>方法来代替。

注意:你不能在keyPathsForValuesAffectingValueForKey:
方法中设立to-many关系的依赖,相反,你必须观察在to-many集合中的每一个对象中相关的属性并通过亲自更新他们的依赖来回应变更。

To-many Relationships

keyPathsForValuesAffectingValueForKey:
方法不支持包含to-many关系的keypath。比如,假如你有一个Department类,它有一个针对Employee
类的to-many关系(雇员),Employee类有salary
属性。你希望Department类有一个totalSalary
属性来计算所有员工的薪水,也就是在这个关系中Department
的totalSalary依赖于所有Employee的salary属性。你不能通过实现keyPathsForValuesAffectingTotalSalary方法并返回employees.salary

有两种解决方法:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
 
    if (context == totalSalaryContext) {
        [self updateTotalSalary];
    }
    else
    // deal with other observations and/or invoke super...
}
 
- (void)updateTotalSalary {
    [self setTotalSalary:[self valueForKeyPath:@"employees.@sum.salary"]];
}
 
- (void)setTotalSalary:(NSNumber *)newTotalSalary {
 
    if (totalSalary != newTotalSalary) {
        [self willChangeValueForKey:@"totalSalary"];
        _totalSalary = newTotalSalary;
        [self didChangeValueForKey:@"totalSalary"];
    }
}
 
- (NSNumber *)totalSalary {
    return _totalSalary;
}

调试KVO

你可以在 lldb 里查看一个被观察对象的所有观察信息。

(lldb) po [observedObject observationInfo]

这会打印出有关谁观察谁之类的很多信息。
这个信息的格式不是公开的,我们不能让任何东西依赖它,因为苹果随时都可以改变它。不过这是一个很强大的排错工具。

转载链接

上一篇 下一篇

猜你喜欢

热点阅读