KVC和KVO(一)

天蓝色的世界

天蓝色的世界

发表于 2016-12-29 17:42:23
内容来源: 网络

欢迎大家关注我的公众号,我会定期分享一些我在项目中遇到问题的解决办法和一些iOS实用的技巧,现阶段主要是整理出一些基础的知识记录下来

文章也会同步更新到我的博客:
http://ppsheep.com

观察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;
}

我们来看一下效果

源工程:

https://github.com/yangqian11...和KVO(一)

参考:

https://www.objccn.io/issue-7-3/

内容来源:https://segmentfault.com/a/1190000007956508

相关帖子
用户评论
开源开发学习小组列表