注册键-值观察
为了接收某个属性的键-值观察通知,以下三个要素是必须的:
- 被观察的类当中你关心的属性必须是遵循键-值观察的,这一细节在
中有所讨论。
- 你必须使用以下方法,将观察方对象与被观察方对象注册:
:forKeyPath:options:context:
. - 观察方的对象必须实现以下方法:
observeValueForKeyPath:ofObject:change:context:
.
内容导航:
注册为观察者
为了正确接收属性的变更通知,观察对象必须首先发送一个addObserver:forKeyPath:options:context:
消息至被观察对象,用以传送观察对象和需要观察的属性的关键路径,以便与其注册。选项参数指定了发送变更通知时提供给观察者的信息。使用NSKeyValueObservingOptionOld
选项可以将初始对象值以变更字典中的一个项的形式提供给观察者。指定NSKeyValueObservingOptionNew
选项可以将新的值以一个项的形式添加至变更字典。你可以使用逐位OR
这两个常量来指定接收上述两种类型的值。
列表1中的例子演示了为属性openingBalance
注册一个检查器对象的方法。
表1 将检查器注册为openingBalance属性的观察者
- (void)registerAsObserver |
{ |
// register "inspector" to receive change notifications |
// for the "openingBalance" property of the "account" object |
// and that both the old and new values of "openingBalance" |
// should be provided to the observer |
[account addObserver:inspector |
forKeyPath:@"openingBalance" |
options:(NSKeyValueObservingOptionNew | |
NSKeyValueObservingOptionOld) |
context:NULL]; |
} |
当你注册一个对象为观察者时,还可以同时指定一个内容指针。当observeValueForKeyPath:ofObject:change:context:
被调用时,内容指针会被提交至观察者。该指针可为C语言指针或对象引用。该指针可作为指定被观察变更的唯一标示符,或是为观察者提供其他数据。该指针不会被保留,应用程序本身应保证该指针在观察方象被取消观察者身份之前被释放。
注意:键-值观察方法addObserver:forKeyPath:options:context:
不会保留观察和被观察对象。你需要检查你的程序需求并管理观察和被观察对象的保留及释放。
接收变更通知
当对象的一个被观察属性发生变动时,观察者收到一个observeValueForKeyPath:ofObject:change:context:
消息。所有观察者都必须实现这一方法。触发观察通知的对象和键路径、包含变更细节的字典,以及观察者注册时提交的上下文指针均被提交给观察者。
变更字典项NSKeyValueChangeKindKey
供发生变更的类型信息。如果被观察对象的值改变了,NSKeyValueChangeKindKey
项将返回一个NSKeyValueChangeSetting
。依赖观察者注册时指定的选项,字典中的NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
项分别包含了被观察属性变更前后的值。
如果被观察的属性是一个对多的关系,NSKeyValueChangeKindKey
项同样可以通过返回三个不同的项NSKeyValueChangeInsertion
,NSKeyValueChangeRemoval
或NSKeyValueChangeReplacement
来分别指明关系中的对象被执行的插入、删除和替换操作。
变更字典项NSKeyValueChangeIndexesKey
是一个指定关系表变更索引的NSIndexSet
对象。注册观察者时,如果NSKeyValueObservingOptionNew
或NSKeyValueObservingOptionOld
被指定为选项,变更字典中NSKeyValueChangeOldKey
和NSKeyValueChangeNewKey
两个项将以数组形式包含相关对象在变更前后的值。
表2中的例子演示了observeValueForKeyPath:ofObject:change:context:
方法实现了在检查器中反映openingBalance
属性的旧值和新值,该属性在中已被注册。
表2 observeValueForKeyPath:ofObject:change: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 super implementation |
// if the superclass implements it |
[super observeValueForKeyPath:keyPath |
ofObject:object |
change:change |
context:context]; |
} |
移除对象的观察者身份
你可以发送一条指定观察方对象和键路径的removeObserver:forKeyPath:
消息至被观察的对象,来移除一个键-值观察者。表3中的例子将检查器移除了其针对openingBalance
的观察者身份。
表3 移除检查器针对openingBalance的观察者身份
- (void)unregisterForChangeNotification |
{ |
[observedObject removeObserver:inspector |
forKeyPath:@"openingBalance"]; |
} |
如果在观察者被注册时,内容被指定为一个对象,那么在观察者被移除后它可以被安全地释放。当接收到removeObserver:forKeyPath:
消息后,观察方对象不会再收到关于指定的关键值路径和对象的任何 observeValueForKeyPath:ofObject:change:context:
消息。
自动支持和手动支持的对比
使用键-值观察,有两种方式可以实现类属性的可观察性。NSObject提供自动观察功能,该功能对遵循键-值编程的类所有属性可用。手工观察在观察行为被通知时提供额外的控制,但需要编写额外的代码。
内容导航:
自动的键-值观察
NSObject提供了基本的自动进行键-值观察的实现方法。使用自动观察通知可在通过键-值编程和遵循键-值编程方法实现功能时免去对变更的归类,进而无须对willChangeValueForKey:
和didChangeValueForKey:
进行请求调用。自动观察者通知受类方法automaticallyNotifiesObserversForKey:
控制。默认的实现方法对所有的键值都返回YES
。
和键-值编码方法一样,自动的键-值观察将遵循键-值的访问器作出的变更通知给观察者。表1中的例子可实现当属性name
发生变更时,其所有观察者都收到变更通知。
表1 调用键-值观察的方法
// calling the accessor method |
[self setName:@"Savings"]; |
// using setValue:forKey: |
[self setValue:@"Savings" forKey:@"name"]; |
// using a key path, where account is a kvc-compliant property |
// of "document" |
[document setValue:@"Savings" forKeyPath:@"account.name"] |
自动通知还支持mutableArrayValueForKey:
和mutableSetValueForKey:
返回集合代理对象。这个功能可用于支持insertObject:in<Key>AtIndex:
,replaceObjectIn<Key>AtIndex:
和removeObjectFrom<Key>AtIndex:
等索引存取方法的对多关系。
你可以通过实现类方法automaticallyNotifiesObserversForKey:
来控制你的子类的自动观察通知 。子类可以检测参数检测的键值,并在自动通知可用时返回YES
,禁用时则返回NO
。
手动观察者通知
手动键-值观察通知针对通知发送至观察者的时间和方式提供更为精确的控制。这可以有效减少无用的触发通知,或是将一组变更集成至一个单独的通知中。
实现手动观察通知的类必须重新实现NSObject类中的automaticallyNotifiesObserversForKey:
方法。在同一个类中同时使用自动和手动观察者通知是可行的。对于执行手动观察者通知的属性来说,子类中automaticallyNotifiesObserversForKey:
实现应当返回NO
。子类实现中对于未在子类中不能识别的键值,必须调用Super
。表2中的例子对 openingBalance
属性启用了手动通知,允许父类确定其它所有键的通知。
表2 automaticallyNotifiesObserversForKey:的实现方法例
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey { |
BOOL automatic = NO; |
if ([theKey isEqualToString:@"openingBalance"]) { |
automatic=NO; |
} else { |
automatic=[super automaticallyNotifiesObserversForKey:theKey]; |
} |
return automatic; |
} |
为了实现手动观察通知,你必须在改变值之前调用willChangeValueForKey:
并在更改它之后调用didChangeValueForKey:
。表3中的例子为实现了属性openingBalance
的手动观察者通知。
表3 实现手动观察者通知的访问器实例
- (void)setOpeningBalance:(double)theBalance { |
[self willChangeValueForKey:@"openingBalance"]; |
openingBalance=theBalance; |
[self didChangeValueForKey:@"openingBalance"]; |
} |
你可以先确认值是否发生了变更,以尽量减小发送无用通知的数量。表4中的例子测试了openingBalance
的值,并且只在该值改变时发送通知。
表4 提交通知前测试值变更状态
- (void)setOpeningBalance:(double)theBalance { |
if (theBalance != openingBalance) { |
[self willChangeValueForKey:@"openingBalance"]; |
openingBalance=theBalance; |
[self didChangeValueForKey:@"openingBalance"]; |
} |
} |
如果某个单一的操作造成了多个键变动,你必须将所有变更通知整合到一起,如表5所示。
表5 整合多个键的通知
- (void)setOpeningBalance:(double)theBalance { |
[self willChangeValueForKey:@"openingBalance"]; |
[self willChangeValueForKey:@"itemChanged"]; |
openingBalance=theBalance; |
itemChanged=itemChanged+1; |
[self didChangeValueForKey:@"itemChanged"]; |
[self didChangeValueForKey:@"openingBalance"]; |
} |
在对多关系情况下,你不仅需要指定被改动的key,同样地还需要指定变更类型和涉及到的对象索引。变更类型是指定NSKeyValueChangeInsertion
,SKeyValueChangeRemoval
或 NSKeyValueChangeReplacement
的NSKeyValueChange
。受影响对象的索引将以NSIndexSet形式传递。
表6中的代码段描述了在对多关系transactions
中内嵌对象删除的方法。
表6 在对多关系中实现手动观察者通知
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes { |
[self willChange:NSKeyValueChangeRemoval |
valuesAtIndexes:indexes forKey:@"transactions"]; |
// remove the transaction objects at the specified indexes here |
[self didChange:NSKeyValueChangeRemoval |
valuesAtIndexes:indexes forKey:@"transactions"]; |
} |
注释:请注意,在发送willChange
消息前,不要释放将发生改动的值。
注册依赖键
在许多情形下,一个属性的值依赖于另一实体中的一个或多个属性。如果其中一个属性的值发生变更,被依赖的属性值也应当为其改动进行标记。保证各个依赖属性的键-值观察通知被正确通报的办法基于你使用的Mac OS X版本和对一或对多关系。
内容导航:
Mac OS X v10.5及更新版本中的对一关系
如果你针对Mac OS X v10.5及更新版本的操作系统,并且需要处理拥有对一关系的相关实体,那么你既可以重写keyPathsForValuesAffectingValueForKey:
也可以实现一个适当的方法来注册依赖的键。
例如,完整的人名依赖于名和姓两者。一个返回全名的方法可以写作如下形式:
- (NSString *)fullName { |
return [NSString stringWithFormat:@"%@ %@",firstName, lastName]; |
} |
在firstName
或lastName
其中任一属性变动时,观察fullName
属性的程序必须被通知,因为它们影响了该属性的值。
一个解决办法是重载keyPathsForValuesAffectingValueForKey:
指明人的fullName
属性是依赖于lastName
和firstName
这两个属性的。演示了实现这种依赖关系的方法:
表1 keyPathsForValuesAffectingValueForKey:
的示例实现方法
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key |
{ |
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key]; |
if ([key isEqualToString:@"fullName"]) |
{ |
NSSet *affectingKeys = [NSSet setWithObjects:@"lastName", @"firstName",nil]; |
keyPaths = [keyPaths setByAddingObjectsFromSet:affectingKeys]; |
} |
return keyPaths; |
} |
你的重载通常应该调用父类并且返回包换父类返回的所有成员的集合(以便让你的重载不影响父类中这个方法)。
你也可以通过实现一个遵循命名规则的类方法keyPathsForvaluesAffecting<Key>
以达到同样的效果,其中 <Key>
是依赖于值的属性名(首字母大写)。使用这种规则,表1中的代码可被重载为一个名为 keyPathsForValuesAffectingFullName
的类方法,如 所示。 表2 keyPathsForValuesAffecting<Key>
命名规则实例实现方法
+ (NSSet *)keyPathsForValuesAffectingFullName |
{ |
return [NSSet setWithObjects:@"lastName", @"firstName", nil |
} |
当你为一个已经存在的使用范畴的类增加一个合成属性是,你不能重载keyPathsForValuesAffectingValueForKey:
方法,原因是你不能重载范畴中的方法。在这种情况下,实现一个匹配keyPathsForValuesAffecting<Key>
的类方法来实现这种机制。
注释:你不能依靠实现keyPathsForValuesAffectingValueForKey:
来为对多关系设置依赖性,而必须观察对多关系集合中每个对象的适当属性,并自己通过更新依赖键来相应值的变更。下一小节说明了处理这一情况的方法。
Mac OS X v10.4和Mac OS X v10.5上的对多关系情况
如果你针对的是Mac OS X v10.4,setKeys:triggerChangeNotificationsForDependentKey:
并不允许任何键路径,因此你不能使用上述的方法。
如果你针对Mac OS X v10.5,keyPathsForValuesAffectingValueForKey:
不允许包含对多关系的键路径。举例来说,假如你有一个Department实体( employees
) , 和Employees之间是对多关系,并且Employee拥有一个salary属性。你可能希望Department实体拥有一个 totalSalary
属性,依赖于所有雇员的薪金水平而变更。此时你无法使用 keyPathsForValuesAffectingTotalSalary
来实现方法并返回键 employees.salary
。 针对以上两种情况,有以下两种可行的解决办法:
- 可以使用键-值观察来注册上级(此例中为Department)为所有下属对象(此例中为Employees)的相关属性的观察者。你必须在添加或移除这个关系中的下属对象的同时添加或移除这个上级对象的观察者的身份。在
方法中你需要响应改变来更新依赖的值,如下面的代码片段所示:observeValueForKeyPath:ofObject:change:context:
- (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 release];
totalSalary = [newTotalSalary retain];
[self didChangeValueForKey:@"totalSalary"];
}
}
- (NSNumber *)totalSalary
{
return totalSalary;
}
- 如果你使用Core Data,可以使用应用的通知中心来注册上级对象为其所管理对象内容的观察者。上级对象应该针对相关的变变更知作出响应,这些变更通知由下级对象以类似于键-值观察的方法发出。