KVO
Key-Value observing(KVC),键值观察,它提供一种机制,当被观察的对象的属性被修改后,KVO会自动通知相对应的观察者。接下来我会演示一下KVO的例子。
观察Model里的属性变化
废话不多说,直接上代码。现有Model: Person
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@end
我们来通过KVO动态监听Person中name, age的变化。这里是标准的操作流程:
第一步:注册,指定被观察者对象
[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
[self.person addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionNew context:nil];
第二步:实现回调方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:@"name"]) {
self.titleLabel.text = self.person.name;
}
}
第三步:移除观察者,如果没有移除观察者,会引发异常,这也是KVO不方便的一点
[self.person removeObserver:self forKeyPath:@"name"];
[self.person removeObserver:self forKeyPath:@"age"];
如上:实现这三部,一个简单的KVO,我们就搞定了。是不是 so easy!
经典案例
下拉刷新,通过监听 frame 来改变动画效果
创建 TableViewController,自定义UIRefreshControl,在这里边监听 frame,来改变动画效果
[self addObserver:self forKeyPath:@"frame" options:NSKeyValueObservingOptionNew context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (self.frame.origin.y > 0 ) {return;}
if (self.frame.origin.y < -60 ) {
[UIView animateWithDuration:0.5 animations:^{
self.refreshView.arrowView.transform = CGAffineTransformRotate(self.refreshView.arrowView.transform, M_PI);
self.refreshView.titleLabel.text = @"下拉刷新";
} completion:^(BOOL finished) {
}];
}else if (self.frame.origin.y >= -60){
[UIView animateWithDuration:0.5 animations:^{
self.refreshView.arrowView.transform = CGAffineTransformRotate(self.refreshView.arrowView.transform, M_PI);
self.refreshView.titleLabel.text = @"松手返回";
} completion:^(BOOL finished) {
}];
}
}
关联依赖
是不是说,掌握了第一部分,我们就能是使用KVO了。答案是肯定的,如果各位看官不想在继续了解的话,请按左上角【返回】按钮。哈哈哈,开玩笑啦,接下来给大家介绍一个好玩的例子,关联依赖。
废话不多说,直接上代码,我就是这么痛快
假设有Person类,其中有三个属性: firstName, lastName, fullName,那当我们要动态的观察 fullName时,怎么办呢。我们都知道fullName 是跟 firstName, lastName有关联的,依据上文说到的方法,这时我们要注册三个观察对象,这很low,这时,依赖关联可以登场了。
第一步: 重写 + (NSSet *)keyPathsForValuesAffection<键名>方法
// 设置 fullName 依赖 firstName, lastName
// 注册观察 fullName, 当firstName, lastName变化时,就能收到通知
+ (NSSet *)keyPathsForValuesAffectingFullName {
NSSet *set = [NSSet setWithObjects:@"firstName",@"lastName", nil];
return set;
}
第二步:之后操作同上
[self.person addObserver:self forKeyPath:@"fullName" options: NSKeyValueObservingOptionPrior context:nil];
这时,我们在改变 firstName, lastName中任意一个属性值的时候,我们通过注册的 fullName 都可以获取到,是不是很神奇啊!
手动通知 VS 自动通知
有没有感觉KVO很神奇,但实际上发生的事情是:当 name 的实例 -setName: 方法被调用的时候,系统自动在之前与之后帮我们添加了部分代码:
- (void)willChangeValueForKey:(NSString *)key
- (void)didChangeValueForKey: (NSString *)key
他们分别会在 setName: 中的代码之前与之后调用。有些时候,我们需要自己来控制是否发送通知,或者改变某些特殊的数据内容,就可以做如下操作:
第一步:重写 setter 方法,并手动添加通知前后方法
-(void)setName:(NSString *)name {
[self willChangeValueForKey:@"name"];
// 这里可以对处理特别操作
_name = [NSString stringWithFormat:@"%@123", name];
[self didChangeValueForKey:@"name"];
}
第二步: 实现 automaticallyNotifiesObserversForKey,return NO
// 通过此方法,决定是否发送通知
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
// 如果是 name 则,手动发送通知
if ([key isEqualToString:@"name"]) {
return NO;
}
// 其他,则自动发送通知
return [super automaticallyNotifiesObserversForKey:key];
}
参数详解
addObserver:forKeyPath: options:context:
observer:观察者,一般都是 self(本身)
keyPath: 被观察的属性名称,例如上文中的 @"name", @"age"
options: 观察属性的新值、旧值等的一些配置(枚举值)
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial = 0x04,
NSKeyValueObservingOptionPrior = 0x08
四个值的含义如下:
NSKeyValueObservingOptionNew:接收方法中使用change参数传入变化后的新值,键为:NSKeyValueChangeNewKey;
NSKeyValueObservingOptionOld:接收方法中使用change参数传入变化前的旧值,键为:NSKeyValueChangeOldKey;
NSKeyValueObservingOptionInitial:注册之后立刻调用接收方法,如果配置了NSKeyValueObservingOptionNew,change参数内容会包含新值,键为:NSKeyValueChangeNewKey;
NSKeyValueObservingOptionPrior:如果加入这个参数,接收方法会在变化前后分别调用一次,共两次,可以分别获取变化前后不通的值
context: 上下文,可以为kvo的回调方法传值
observeValueForKeyPath: ofObject: change:context:
keyPath:属性名称,通过它可以获取到当前观察的是哪个属性
object:被观察的对象
change:变化前后的值都存储在change字典中
context:注册观察者时,context传过来的值
线程
KVO 是线程同步的,发生变化的值与所观察的值在同一个线程上。
我们可以在改变被观察对象的值时,来开启一个线程,在回调方法中查看是否同属于同一个线程。
[self performSelectorInBackground:@selector(changeValue) withObject:nil];
- (void)changeValue {
self.starThreadLabel.text = [NSString stringWithFormat:@"开启线程:%@", [NSThread currentThread]];
self.person.name = @"我是子线程"
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
NSLog(@"当前线程为:%@",[NSThread currentThread]);
}
PS: 以上都是一家之言,望各位看官多加实验来验证,非喜勿喷。