Key-value coding (KVC) 和 key-value observing (KVO) 是两种能让我们驾驭 Objective-C 动态特性并简化代码的机制。在这篇文章里,我们将接触一些如何利用这些特性的例子。

 

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

}

现在我们完整的表达了属性之间的依赖关系。请注意,我们可以把这些属性链接起来。打个比方,如果我们写一个子类去 override redComponent 方法,这些依赖关系仍然能正常工作。

 

观察变化

现在让我们目光转向控制器。 NSViewController 的子类拥有 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