欢迎大家关注我的公众号,我会定期分享一些我在项目中遇到问题的解决办法和一些iOS实用的技巧,现阶段主要是整理出一些基础的知识记录下来
文章也会同步更新到我的博客:
观察model对象的变化
在 Cocoa 的模型-视图-控制器 (Model-view-controller)架构里,控制器负责让视图和模型同步。这一共有两步:当 model 对象改变的时候,视图应该随之改变以反映模型的变化;当用户和控制器交互的时候,模型也应该做出相应的改变。
KVO 能帮助我们让视图和模型保持同步。控制器可以观察视图依赖的属性变化。
让我们看一个例子:我们的模型类 LabColor 代表一种 Lab色彩空间里的颜色。和 RGB 不同,这种色彩空间有三个元素 L, a, b。我们要做一个用来改变这些值的滑块和一个显示颜色的方块区域。
我们的模型类有以下三个用来代表颜色的属性:
@property (nonatomic) double lComponent;@property (nonatomic) double aComponent;@property (nonatomic) double bComponent;
依赖的属性
我们需要从这个类创建一个 UIColor 对象来显示出颜色。我们添加三个额外的属性,分别对应 R, G, B:
@property (nonatomic, readonly) double redComponent;@property (nonatomic, readonly) double greenComponent;@property (nonatomic, readonly) double blueComponent;@property (nonatomic, strong, readonly) UIColor *color;
有了这些以后,我们就可以创建这个类的接口了:
@interface LabColor : NSObject@property (nonatomic) double lComponent;@property (nonatomic) double aComponent;@property (nonatomic) double bComponent;@property (nonatomic, readonly) double redComponent;@property (nonatomic, readonly) double greenComponent;@property (nonatomic, readonly) double blueComponent;@property (nonatomic, strong, readonly) UIColor *color;@end
维基百科提供了转换 RGB 到 Lab 色彩空间的算法。写成方法之后如下所示:
- (double)greenComponent;{ return D65TristimulusValues[1] * inverseF(1./116. * (self.lComponent + 16) + 1./500. * self.aComponent);}[...]- (UIColor *)color{ return [UIColor colorWithRed:self.redComponent * 0.01 green:self.greenComponent * 0.01 blue:self.blueComponent * 0.01 alpha:1.];}
这些代码没什么令人激动的地方。有趣的是 greenComponent 属性依赖于 lComponent 和 aComponent。不论何时设置 lComponent 的值,我们需要让 RGB 三个 component 中与其相关的成员以及 color 属性都要得到通知以保持一致。这一点这在 KVO 中很重要。
Foundation 框架提供的表示属性依赖的机制如下:
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
更详细的如下:
+ (NSSet *)keyPathsForValuesAffecting <键名>键名>
在我们的例子中如下:
+ (NSSet *)keyPathsForValuesAffectingRedComponent{ return [NSSet setWithObject:@"lComponent"];}+ (NSSet *)keyPathsForValuesAffectingGreenComponent{ return [NSSet setWithObjects:@"lComponent", @"aComponent", nil];}+ (NSSet *)keyPathsForValuesAffectingBlueComponent{ return [NSSet setWithObjects:@"lComponent", @"bComponent", nil];}+ (NSSet *)keyPathsForValuesAffectingColor{ return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];}
注意
这里解释一下,可能有一些朋友对这里的对象依赖关系不是特别清楚。我们拿出一个来解释
+ (NSSet *)keyPathsForValuesAffectingGreenComponent{ return [NSSet setWithObjects:@"lComponent", @"aComponent", nil];}
重要
在这个方法中,它代表的意思是当lComponent属性,或者aComponent属性改变时,需要通知到greenComponent,就相当于,lComponent或者aComponent中的任何一个作出改变时,可以认为greenComponent也发生了改变,如果这时,有一个监听对象,在监听greenComponent,那么当我们改变lComponent或aComponent,这个监听会被触发
至于这个方法怎么得来的,在peoperty中定义了该属性,你只要敲keyPath就会自动提示出来,有哪些可以设置依赖了
在我们的color中
+ (NSSet *)keyPathsForValuesAffectingColor{ return [NSSet setWithObjects:@"redComponent", @"greenComponent", @"blueComponent", nil];}
color是依赖于redComponent、greenComponent和blueComponent,这样 我们就能知道,改变lComponent、aComponent、bComponent任何一个,都会触发color的监听
现在我们完整的表达了属性之间的依赖关系。请注意,我们可以把这些属性链接起来。打个比方,如果我们写一个子类去 override redComponent 方法,这些依赖关系仍然能正常工作。
观察变化
现在让我们目光转向控制器。 UIViewController 的子类拥有 LabColor model 对象作为其属性。
@interface ViewController ()@property (nonatomic, strong) LabColor *labColor;@end
我们把视图控制器注册为观察者来接收 KVO 的通知,这可以用以下 NSObject 的方法来实现:
- (void)addObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context
这会让以下方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
在当 keyPath 的值改变的时候在观察者 anObserver 上面被调用。这个 API 看起来有一点吓人。更糟糕的是,我们还得记得调用以下的方法
- (void)removeObserver:(NSObject *)anObserver forKeyPath:(NSString *)keyPath
来移除观察者,否则我们我们的 app 会因为某些奇怪的原因崩溃。
对于大多数的应用来说,KVO 可以通过辅助类用一种更简单优雅的方式实现。我们在视图控制器添加以下的观察记号(Observation token)属性:
@property (nonatomic, strong) id colorObserveToken;
当 labColor 在视图控制器中被设置时,我们只要 override labColor 的 setter 方法就行了:
- (void)setLabColor:(LabColor *)labColor{ _labColor = labColor; self.colorObserveToken = [KeyValueObserver observeObject:labColor keyPath:@"color" target:self selector:@selector(colorDidChange:) options:NSKeyValueObservingOptionInitial];}- (void)colorDidChange:(NSDictionary *)change;{ self.colorView.backgroundColor = self.labColor.color;}
我们封装一个KeyValueObserver辅助类
KeyValueObserver 辅助类 封装了 -addObserver:forKeyPath:options:context:,-observeValueForKeyPath:ofObject:change:context:和-removeObserverForKeyPath: 的调用,让视图控制器远离杂乱的代码。
// 其中__attribute__((warn_unused_result))这个的意思是,当调用这个方法时,必须要检查返回值,或者使用返回值,不然编译器直接报警告+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector __attribute__((warn_unused_result));+ (NSObject *)observeObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector options:(NSKeyValueObservingOptions)options __attribute__((warn_unused_result));
整合到一起
视图控制器需要对 L,a,b 的滑块控制做出反应:
- (void)updateLComponent:(UISlider *)sender;{ self.labColor.lComponent = sender.value;}- (void)updateAComponent:(UISlider *)sender;{ self.labColor.aComponent = sender.value;}- (void)updateBComponent:(UISlider *)sender;{ self.labColor.bComponent = sender.value;}
我们来看一下效果
源工程:
和KVO(一)
参考:
https://www.objccn.io/issue-7-3/