使用属性名或属性路径来访问类的属性。
key,就是@”属性名”
keyPath,就是属性的路径,@”属性名.属性名“。
什么意思呢?
已知一个类,定义了属性NSString *name
和一个结构体变量person(person中有一个变量为age)。
我们假设这个类有个对象是p;
那么我们要访问p的变量name,可以用@“name”:
[p valueForKey:@“name”];
要访问p的变量person,当然也可以用@“person”,那么如果要访问person结构体变量中的age呢,可以用@“person.age”:
[p valueForKeyPath:@“person.age”];
就是这么简单!
以上两个是相当于getter,当然也有相当于setter的方法:
从上面可以看出,这里把key看做属性名,而value就作为属性值。由此可以延伸到另外一个很灵活的方法。
我们知道NSDictionary
就是存储键值对的,如果可以把NSDictionary的key作为这里的key,把NSDictionary的value作为这里的value,就可以实现一次性给对象的多个属性赋值了!
确实有这样的方法。
新建字典的方法不多说,注意把@“属性名”对应放在字典的key位置,把要赋给属性的值(或对象)对应放在字典的value位置:
NSDictionary *keyAndValues=@[@“属性1”:对象1,@“属性2”:对象2, … ,@“属性n”:对象n]
这里需要注意,如果要访问的属性实际上是基本类型,而不是对象,则通过key来访问获得的是一个NSValue对象,属性值就封装在里面。对于下面的KVO也是一样。
使用KVC技术是有前提的,比如这个属性要有默认的setter方法set<属性名>,等等这里不细说。可以参见:https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-SW1
将某个对象和另外一个对象的属性关联起来,当一个属性(被监视者)变化的时候,会通知另外一个属性(监视器)。
NSObject类已经实现了KVO,因此可以说所有的Cocoa对象都继承了KVO的功能。
KVO里面,一个属性被监视(Observed),一个属性是监听器(Observer),要KVO功能正常发挥作用的前提是这两者都能够支持KVO。
添加监视者方法:addObserver:forKeyPath:options:context:
比如:为account对象的属性openingBalance注册一个监听器inspector,并(通过options)指定需要带上这个属性的原来的值和新的值。
account addObserver:inspector forKeyPath:@“openingBalance" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
context是指通知中附带的消息(类型为指针),这里选择了不传送附带消息。
每当监视的属性有变化,监听器都都用这个方法,因此所有的监听器都需要实现这个方法observeValueForKeyPath:ofObject:change:context:
那么如何访问这个dictionary对象以获得有关变化的信息呢?
可以通过这些入口:也就是key
NSKeyValueChangeKindKey 对应一个NSNumber对象,它的值对应了枚举类型NSKeyValueChange中的某一个值,这个枚举类型对所发生的变化划分了几个种类。
NSKeyValueChangeIndexesKey 对应一个NSIndexSet对象,存储了发生变化的集合元素的索引值。
NSKeyValueChangeOldKey,NSKeyValueChangeNewKey 对应了的是数组,里面存储了相关变量的原来的值,变化后的值,还有所发生的变化。
一个实现该方法的例子:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqual:@"openingBalance"]) {
_balance= [change objectForKey:NSKeyValueChangeNewKey];
}
/* 如果父类实现了这个方法的话,记得要调用一下父类的这个方法 NSObject没有实现这个方法,如果父类是NSObject的话不需要调用下面这段。 */
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
实际上,在被监听的属性发生变化以后,除了会执行上面实现了的方法,还会将上面方法中被改变的对象曾经参与过的动作都重新执行一遍。比如,在openingBalance发生变化之前,就曾执行过NSLog(@“%@”,inspector.balance);
,那么在openingBalance发生变化以后,自动会再次输出inspector.balance的值,而不需要额外添加代码,也就是说,有一个自动更新的机制在里面。而且,这些更新的操作是紧接着被监听对象的改变之后执行的,只有执行完这些操作才会去执行,修改被监听对象的语句之后的语句。
还要注意,被监听的属性如果是个类的对象,那么通过change查询到的就是这个对象,如果被监听的属性是C的基本类型,或者是标量(如NSInteger),返回的则是封装了这个量的NSValue对象。
- (void)unregisterForChangeNotification {
[observedObject removeObserver:inspector forKeyPath:@"openingBalance"];
}
变化发生后,触发通知的方式有两种:Automatic Change Notification和Manual Change Notification(自动通知和手动通知)。
自动通知是由NSObject提供的,所以所有遵守KVC条件的子类都具有这个能力。
也就是,一调用setter方法,或者通过KVC方法来改变属性,就会触发变化通知,自动的。
手动通知:需要实现手动通知的类必须重写NSObject的自动通知方法,也就是automaticallyNotifiesObserversForKey:方法。
重写这个方法的目的是确定某个属性是使用自动通知还是手动通知:
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey {
BOOL automatic = NO;
if ([theKey isEqualToString:@"openingBalance"]) {
automatic = NO;
}
else {
automatic = [super automaticallyNotifiesObserversForKey:theKey];
}
return automatic;
}
这上面的方法(注意是类方法哦!),指定只有openingBalance变化的时候才使用手动通知,其他的属性采用自动通知。返回值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"];
}
如果改变的是一对多的关系,具体来说,就是去改变类型为集合的属性,比如改变集合内部的对象,那么要怎么去触发通知呢:
- (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"];
}
可见,是与willChangeValueForKey和didChangeValueForKey相对的,要改变集合的元素,需要调用的是willChange:valueAtIndexes:forKey:
和didChange:valueAtIndexes:forKey:
。这两个方法和前面两个的区别在于,多了两个参数:变化种类,和要改变的元素的下标。
这里的变化种类,就会存到在通知中讲到的change字典中与key“NSKeyValueChangeKindKey”对应的NSNumber对象中;这里要改变的元素的下标,就存到与key“NSKeyValueChangeIndexesKey”对应的NSIndexSet对象里面。
要改变的种类,也就是之前说到的枚举类型,有3个值:NSKeyValueChangeInsertion, NSKeyValueChangeRemoval, 和NSKeyValueChangeReplacement。
上面都是一个对象监听另外一个对象,在实际运用中,也有很多情况是一个监听多个对象,比如说,一个人的全名,由姓和名组成,那么全名这个对象,就需要同时监听姓对象和名对象。
这种情况下,不能用observeValueForKeyPath:ofObject:change:context:
方法了,因为这个方法只能为调用者添加一个监听者。我们需要实现另外一个方法,为调用者添加多个被监听的对象:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
if ([key isEqualToString:@"fullName"]) {
NSArray *affectingKeys = @[@"lastName", @"firstName"];
keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
}
return keyPaths;
}
上面的参数key就是对其他属性的依赖者,返回值是一个NSSet(不允许重复元素),里面存储的就是依赖者要依赖的多个对象的key。其中先用NSArray来承接,显然是防止key有重复,然后再把NSArray的元素添加到由父类方法返回的NSSet中。
从代码中还可以看出,这个方法不单单针对fullName这个属性,全部需要添加多个被监听对象的依赖者都应该在这里得到处理。主要是通过if语句来区别不同的依赖者。
当然我们也可以单独地为每个依赖者写一个方法,而不是一起写。要求是方法的命名需要遵循一定的原则:keyPathsForValuesAffecting<Key>
,这里的Key就是依赖者的属性名。比如对于fullName,我们可以实现方法
keyPathsForValuesAffectingFullName,方法的实现简单多了:
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"lastName", @"firstName", nil];
}
(一种特殊情况是,如果依赖者是定义在一个类的category里面的话,就不能重写keyPathsForValuesAffectingValueForKey:方法了,因为不能够重写category的方法。此时,就可以用单独的实现方法keyPathsForValuesAffecting<Key>
来实现。)
那么和前面遇到的情况类似,如果被监听对象是某个集合的元素的话,而在上面的方法中只支持key,不支持keyPath,要怎么办呢?有两种方法可以解决这个问题:
累了,用到再看:
https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVODependentKeys.html#//apple_ref/doc/uid/20002179-BAJEAIEE