[译]KVC 和 KVO详解

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

观察 model 对象的变化

在 Cocoa 的模型-视图-控制器 (Model-view-controller)架构里,控制器负责让视图和模型同步。这一共有两步:当 model 对象改变的时候,视图应该随之改变以反映模型的变化;当用户和控制器交互的时候,模型也应该作出相应的改变。ios

KVO 能帮助咱们让视图和模型保持同步。控制器能够观察视图依赖的属性变化。git

让咱们看一个例子:咱们的模型类 LabColor 表明一种 Lab色彩空间里的颜色。和 RGB 不一样,这种色彩空间有三个元素 L, a, b。咱们要作一个用来改变这些值的滑块和一个显示颜色的方块区域。github

咱们的模型类有如下三个用来表明颜色的属性:算法

@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 属性依赖于 lComponentaComponent。不论什么时候设置 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 = self.labColor.color;
}
复制代码

KeyValueObserver 辅助类 封装了 -addObserver:forKeyPath:options:context:-observeValueForKeyPath:ofObject:change:context:-removeObserverForKeyPath: 的调用,让视图控制器远离杂乱的代码。

整合到一块儿

视图控制器须要对 Lab 的滑块控制作出反应:

- (IBAction)updateLComponent:(UISlider *)sender;
{
    self.labColor.lComponent = sender.value;
}

- (IBAction)updateAComponent:(UISlider *)sender;
{
    self.labColor.aComponent = sender.value;
}

- (IBAction)updateBComponent:(UISlider *)sender;
{
    self.labColor.bComponent = sender.value;
}
复制代码

全部的代码都在咱们的 GitHub 示例代码 中找到。

手动通知 vs 自动通知

咱们刚才所作的事情有点神奇,可是实际上发生的事情是,当 LabColor 实例的 -setLComponent: 等方法被调用的时候如下方法:

- (void)willChangeValueForKey:(NSString *)key
复制代码

和:

- (void)didChangeValueForKey:(NSString *)key
复制代码

会在运行 -setLComponent: 中的代码以前以及以后被自动调用。若是咱们写了 -setLComponent: 或者咱们选择使用自动 synthesize 的 lComponent 的 accessor 到时候就会发生这样的事情。

有些状况下当咱们须要 override -setLComponent: 而且咱们要控制是否发送键值改变的通知的时候,咱们要作如下的事情:

+ (BOOL)automaticallyNotifiesObserversForLComponent;
{
    return NO;
}

- (void)setLComponent:(double)lComponent;
{
    if (_lComponent == lComponent) {
        return;
    }
    [self willChangeValueForKey:@"lComponent"];
    _lComponent = lComponent;
    [self didChangeValueForKey:@"lComponent"];
}
复制代码

咱们关闭了 -willChangeValueForKey:-didChangeValueForKey: 的自动调用,而后咱们手动调用他们。咱们只应该在关闭了自动调用的时候咱们才须要在 setter 方法里手动调用 -willChangeValueForKey:-didChangeValueForKey:。大多数状况下,这样优化不会给咱们带来太多好处。

若是咱们在 accessor 方法以外改变实例对象(如 _lComponent ),咱们要特别当心地和刚才同样封装 -willChangeValueForKey:-didChangeValueForKey:。不过在多数状况下,咱们只用 accessor 方法的话就能够了,这样代码会简洁不少。

KVO 和 context

有时咱们会有理由不想用 KeyValueObserver 辅助类。建立另外一个对象会有额外的性能开销。若是咱们观察不少个键的话,这个开销可能会变得明显。

若是咱们在实现一个类的时候把它本身注册为观察者的话:

- (void)addObserver:(NSObject *)anObserver
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context
复制代码

一个很是重要的点是咱们要传入一个这个类惟一的 context。咱们推荐把如下代码

static int const PrivateKVOContext;
复制代码

写在这个类 .m 文件的顶端,而后咱们像这样调用 API 并传入 PrivateKVOContext 的指针:

[otherObject addObserver:self forKeyPath:@"someKey" options:someOptions context:&PrivateKVOContext];
复制代码

而后咱们这样写 -observeValueForKeyPath:... 的方法:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if (context == &PrivateKVOContext) {
        // 这里写相关的观察代码
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
复制代码

这将确保咱们写的子类都是正确的。如此一来,子类和父类都能安全的观察一样的键值而不会冲突。不然咱们将会碰到难以 debug 的奇怪行为。

进阶 KVO

咱们经常须要当一个值改变的时候更新 UI,可是咱们也要在第一次运行代码的时候更新一次 UI。咱们能够用 KVO 并添加 NSKeyValueObservingOptionInitial 的选项 来一举两得地作好这样的事情。这将会让 KVO 通知在调用 -addObserver:forKeyPath:... 到时候也被触发。

以前和以后

当咱们注册 KVO 通知的时候,咱们能够添加 NSKeyValueObservingOptionPrior 选项,这能使咱们在键值改变以前被通知。这和-willChangeValueForKey:被触发的时间相对应。

若是咱们注册通知的时候附加了 NSKeyValueObservingOptionPrior 选项,咱们将会收到两个通知:一个在值变动前,另外一个在变动以后。变动前的通知将会在 change 字典中有不一样的键。咱们能够像如下这样区分通知是在改变以前仍是以后被触发的:

if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) {
    // 改变以前
} else {
    // 改变以后
}
复制代码

若是咱们须要改变先后的值,咱们能够在 KVO 选项中加入 NSKeyValueObservingOptionNew 和/或 NSKeyValueObservingOptionOld

更简单的办法是用 NSKeyValueObservingOptionPrior 选项,随后咱们就能够用如下方式提取出改变先后的值:

id oldValue = change[NSKeyValueChangeOldKey];
id newValue = change[NSKeyValueChangeNewKey];
复制代码

一般来讲 KVO 会在 -willChangeValueForKey:-didChangeValueForKey: 被调用的时候存储相应键的值。

索引

KVO 对一些集合类也有很强的支持,如下方法会返回集合对象:

-mutableArrayValueForKey:
-mutableSetValueForKey:
-mutableOrderedSetValueForKey:
复制代码

咱们将会详细解释这是怎么工做的。若是你使用这些方法,change 字典里会包含键值变化的类型(添加、删除和替换)。对于有序的集合,change 字典会包含受影响的 index。

集合代理对象和变化的通知在用于更新UI的时候很是有效,尤为是处理大集合的时候。可是它们须要花费你一些心思。

KVO 和线程

一个须要注意的地方是,KVO 行为是同步的,而且发生与所观察的值发生变化的一样的线程上。没有队列或者 Run-loop 的处理。手动或者自动调用 -didChange... 会触发 KVO 通知。

因此,当咱们试图从其余线程改变属性值的时候咱们应当十分当心,除非能肯定全部的观察者都用线程安全的方法处理 KVO 通知。一般来讲,咱们不推荐把 KVO 和多线程混起来。若是咱们要用多个队列和线程,咱们不该该在它们互相之间用 KVO。

KVO 是同步运行的这个特性很是强大,只要咱们在单一线程上面运行(好比主队列 main queue),KVO 会保证下列两种状况的发生:

首先,若是咱们调用一个支持 KVO 的 setter 方法,以下所示:

self.exchangeRate = 2.345;
复制代码

KVO 能保证全部 exchangeRate 的观察者在 setter 方法返回前被通知到。

其次,若是某个键被观察的时候附上了 NSKeyValueObservingOptionPrior 选项,直到 -observe... 被调用以前, exchangeRate 的 accessor 方法都会返回一样的值。

KVC

最简单的 KVC 能让咱们经过如下的形式访问属性:

@property (nonatomic, copy) NSString *name;
复制代码

取值:

NSString *n = [object valueForKey:@"name"]
复制代码

设定:

[object setValue:@"Daniel" forKey:@"name"]
复制代码

值得注意的是这个不只能够访问做为对象属性,并且也能访问一些标量(例如 intCGFloat)和 struct(例如 CGRect)。Foundation 框架会为咱们自动封装它们。举例来讲,若是有如下属性:

@property (nonatomic) CGFloat height;
复制代码

咱们能够这样设置它:

[object setValue:@(20) forKey:@"height"]
复制代码

KVC 容许咱们用属性的字符串名称来访问属性,字符串在这儿叫作。有些状况下,这会使咱们很是灵活地简化代码。咱们下一节介绍例子简化列表 UI

KVC 还有更多能够谈的。集合(NSArrayNSSet 等)结合 KVC 能够拥有一些强大的集合操做。还有,对象能够支持用 KVC 经过代理对象访问很是规的属性。

简化列表 UI

假设咱们有这样一个对象:

@interface Contact : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickname;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *city;

@end
复制代码

还有一个 detail 视图控制器,含有四个对应的 UITextField 属性:

@interface DetailViewController ()

@property (weak, nonatomic) IBOutlet UITextField *nameField;
@property (weak, nonatomic) IBOutlet UITextField *nicknameField;
@property (weak, nonatomic) IBOutlet UITextField *emailField;
@property (weak, nonatomic) IBOutlet UITextField *cityField;

@end
复制代码

咱们能够简化更新 UI 的逻辑。首先咱们须要两个方法:一个返回 model 里咱们用到的全部键的方法,一个把键映射到对应的文本框的方法:

- (NSArray *)contactStringKeys;
{
    return @[@"name", @"nickname", @"email", @"city"];
}

- (UITextField *)textFieldForModelKey:(NSString *)key;
{
    return [self valueForKey:[key stringByAppendingString:@"Field"]];
}
复制代码

有了这个,咱们能够从 model 里更新文本框,以下所示:

- (void)updateTextFields;
{
    for (NSString *key in self.contactStringKeys) {
        [self textFieldForModelKey:key].text = [self.contact valueForKey:key];
    }
}
复制代码

咱们也能够用一个 action 方法让四个文本框都能实时更新 model:

- (IBAction)fieldEditingDidEnd:(UITextField *)sender
{
    for (NSString *key in self.contactStringKeys) {
        UITextField *field = [self textFieldForModelKey:key];
        if (field == sender) {
            [self.contact setValue:sender.text forKey:key];
            break;
        }
    }
}
复制代码

注意:咱们以后会添加验证输入的部分,在键值验证里会提到。

最后,咱们须要确认文本框在须要的时候被更新:

- (void)viewWillAppear:(BOOL)animated;
{
    [super viewWillAppear:animated];
    [self updateTextFields];
}

- (void)setContact:(Contact *)contact
{
    _contact = contact;
    [self updateTextFields];
}
复制代码

有了这个,咱们的 detail 视图控制器 就能正常工做了。

整个项目能够在 GitHub 上找到。它也用了咱们后面提到的键值验证

键路径(Key Path)

KVC 一样容许咱们经过关系来访问对象。假设 person 对象有属性 addressaddress 有属性 city,咱们能够这样经过 person 来访问 city

[person valueForKeyPath:@"address.city"]
复制代码

值得注意的是这里咱们调用 -valueForKeyPath: 而不是 -valueForKey:

Key-Value Coding Without @property

不须要 @property 的 KVC

咱们能够实现一个支持 KVC 而不用 @property@synthesize 或是自动 synthesize 的属性。最直接的方式是添加 -<key>-set<Key>: 方法。例如咱们想要 name ,咱们这样作:

- (NSString *)name;
- (void)setName:(NSString *)name;
复制代码

这彻底等于 @property 的实现方式。

可是当标量和 struct 的值被传入 nil 的时候尤为须要注意。假设咱们要 height 属性支持 KVC 咱们写了如下的方法:

- (CGFloat)height;
- (void)setHeight:(CGFloat)height;
复制代码

而后咱们这样调用:

[object setValue:nil forKey:@"height"]
复制代码

这会抛出一个 exception。要正确的处理 nil,咱们要像这样 override -setNilValueForKey:

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:@"height"]) {
        [self setValue:@0 forKey:key];
    } else
        [super setNilValueForKey:key];
}
复制代码

咱们能够经过 override 这些方法来让一个类支持 KVC:

- (id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;
复制代码

这也许看起来很怪,但这可让一个类动态的支持一些键的访问。可是这两个方法会在性能上拖后腿。

附注:Foundation 框架支持直接访问实例变量。请当心的使用这个特性。你能够去查看 +accessInstanceVariablesDirectly 的文档。这个值默认是 YES 的时候,Foundation 会按照 _<key>, _is<Key>, <key>is<Key> 的顺序查找实例变量。

集合的操做

一个经常被忽视的 KVC 特性是它对集合操做的支持。举个例子,咱们能够这样来得到一个数组中最大的值:

NSArray *a = @[@4, @84, @2];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.self"]);
复制代码

或者说,咱们有一个 Transaction 对象的数组,对象有属性 amount 的话,咱们能够这样得到最大的 amount

NSArray *a = @[transaction1, transaction2, transaction3];
NSLog(@"max = %@", [a valueForKeyPath:@"@max.amount"]);
复制代码

当咱们调用 [a valueForKeyPath:@"@max.amount"] 的时候,它会在数组 a 的每一个元素中调用 -valueForKey:@"amount" 而后返回最大的那个。

KVC 的苹果官方文档有一个章节 Collection Operators 详细的讲述了相似的用法。

经过集合代理对象来实现 KVC

虽然咱们能够像对待通常的对象同样用 KVC 深刻集合内部(NSArrayNSSet 等),可是经过集合代理对象, KVC 也让咱们实现一个兼容 KVC 的集合。这是一个颇为高端的技巧。

当咱们在对象上调用 -valueForKey: 的时候,它能够返回 NSArrayNSSet 或是 NSOrderedSet 的集合代理对象。这个类没有实现一般的 -<Key> 方法,可是它实现了代理对象所须要使用的不少方法。

若是咱们但愿一个类支持经过代理对象的 contacts 键返回一个 NSArray,咱们能够这样写:

- (NSUInteger)countOfContacts;
- (id)objectInContactsAtIndex:(NSUInteger)idx;
复制代码

这样作的话,当咱们调用 [object valueForKey:@"contacts”] 的时候,它会返回一个由这两个方法来代理全部调用方法的 NSArray 对象。这个数组支持全部正常的对 NSArray 的调用。换句话说,调用者并不知道返回的是一个真正的 NSArray, 仍是一个代理的数组。

对于 NSSetNSOrderedSet,若是要作一样的事情,咱们须要实现的方法是:

NSArray NSSet                 NSOrderedSet             
-countOf<Key> -countOf<Key> -countOf<Key>
-enumeratorOf<Key> -indexIn<Key>OfObject:
如下二者二选一 -memberOf<Key>:
-objectIn<Key>AtIndex: 如下二者二选一
-<key>AtIndexes: -objectIn<Key>AtIndex:
-<key>AtIndexes:
可选(加强性能)
-get<Key>:range: 可选(加强性能)
-get<Key>:range:

可选 的一些方法能够加强代理对象的性能。

虽然只有特殊状况下咱们用这些代理对象才会有意义,可是在这些状况下代理对象很是的有用。想象一下咱们有一个很大的数据结构,调用者不须要(一次性)访问全部的对象。

举一个(也许比较作做的)例子说,咱们想写一个包含有很长一串质数的类。以下所示:

@interface Primes : NSObject

@property (readonly, nonatomic, strong) NSArray *primes;

@end



@implementation Primes

static int32_t const primes[] = {
    2, 101, 233, 383, 3, 103, 239, 389, 5, 107, 241, 397, 7, 109,
    251, 401, 11, 113, 257, 409, 13, 127, 263, 419, 17, 131, 269,
    421, 19, 137, 271, 431, 23, 139, 277, 433, 29, 149, 281, 439,
    31, 151, 283, 443, 37, 157, 293, 449, 41, 163, 307, 457, 43,
    167, 311, 461, 47, 173, 313, 463, 53, 179, 317, 467, 59, 181,
    331, 479, 61, 191, 337, 487, 67, 193, 347, 491, 71, 197, 349,
    499, 73, 199, 353, 503, 79, 211, 359, 509, 83, 223, 367, 521,
    89, 227, 373, 523, 97, 229, 379, 541, 547, 701, 877, 1049,
    557, 709, 881, 1051, 563, 719, 883, 1061, 569, 727, 887,
    1063, 571, 733, 907, 1069, 577, 739, 911, 1087, 587, 743,
    919, 1091, 593, 751, 929, 1093, 599, 757, 937, 1097, 601,
    761, 941, 1103, 607, 769, 947, 1109, 613, 773, 953, 1117,
    617, 787, 967, 1123, 619, 797, 971, 1129, 631, 809, 977,
    1151, 641, 811, 983, 1153, 643, 821, 991, 1163, 647, 823,
    997, 1171, 653, 827, 1009, 1181, 659, 829, 1013, 1187, 661,
    839, 1019, 1193, 673, 853, 1021, 1201, 677, 857, 1031,
    1213, 683, 859, 1033, 1217, 691, 863, 1039, 1223, 1229,
};

- (NSUInteger)countOfPrimes;
{
    return (sizeof(primes) / sizeof(*primes));
}

- (id)objectInPrimesAtIndex:(NSUInteger)idx;
{
    NSParameterAssert(idx < sizeof(primes) / sizeof(*primes));
    return @(primes[idx]);
}

@end
复制代码

咱们将会运行如下代码:

Primes *primes = [[Primes alloc] init];
NSLog(@"The last prime is %@", [primes.primes lastObject]);
复制代码

这将会调用一次 -countOfPrimes 和一次传入参数 idx 做为最后一个索引的 -objectInPrimesAtIndex:。为了只取出最后一个值,它不须要先把全部的数封装成 NSNumber 而后把它们都导入 NSArray

在一个复杂一点的例子中,通信录编辑器示例 app 用一样的方法把 C++ std::vector 封装以来。它详细说明了应该怎么利用这个方法。

可变的集合

咱们也能够在可变集合(例如 NSMutableArrayNSMutableSet,和 NSMutableOrderedSet)中用集合代理。

访问这些可变的集合有一点点不一样。调用者在这儿须要调用如下其中一个方法:

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key;
复制代码

一个窍门:咱们可让一个类用如下方法返回可变集合的代理:

- (NSMutableArray *)mutableContacts;
{
    return [self mutableArrayValueForKey:@"wrappedContacts"];
}
复制代码

而后在实现键 wrappedContacts 的一些方法。

咱们须要实现上面的不变集合的两个方法,还有如下的几个:

NSMutableArray / NSMutableOrderedSet        NSMutableSet                             
至少实现一个插入方法和一个删除方法 至少实现一个插入方法和一个删除方法
-insertObject:in<Key>AtIndex: -add<Key>Object:
-removeObjectFrom<Key>AtIndex: -remove<Key>Object:
-insert<Key>:atIndexes: -add<Key>:
-remove<Key>AtIndexes: -remove<Key>:
可选(加强性能)如下方法二选一 可选(加强性能)
-replaceObjectIn<Key>AtIndex:withObject: -intersect<Key>:
-replace<Key>AtIndexes:with<Key>: -set<Key>:

上面提到,这些可变集合代理对象和 KVO 结合起来也十分强大。KVO 机制能在这些集合改变的时候把详细的变化放进 change 字典中。

有批量更新(须要传入多个对象)的方法,也有只改变一个对象的方法。咱们推荐选择相对于给定任务来讲最容易实现的那个来写,虽然咱们有一点点倾向于选择批量更新的那个。

在实现这些方法的时候,咱们要对自动和手动的 KVO 之间的差异十分当心。Foundation 默认自动发出十分详尽的变化通知。若是咱们要手动实现发送详细通知的话,咱们得实现这些:

-willChange:valuesAtIndexes:forKey:
-didChange:valuesAtIndexes:forKey:
复制代码

或者这些:

-willChangeValueForKey:withSetMutation:usingObjects:
-didChangeValueForKey:withSetMutation:usingObjects:
复制代码

咱们要保证先把自动通知关闭,不然每次改变 KVO 都会发出两次通知。

常见的 KVO 错误

首先,KVO 兼容是 API 的一部分。若是类的全部者不保证某个属性兼容 KVO,咱们就不能保证 KVO 正常工做。苹果文档里有 KVO 兼容属性的文档。例如,NSProgress 类的大多数属性都是兼容 KVO 的。

当作出改变之后,有些人试着放空的 -willChange-didChange 方法来强制 KVO 的触发。KVO 通知虽然会生效,可是这样作破坏了有依赖于 NSKeyValueObservingOld 选项的观察者。详细来讲,这影响了 KVO 对观察键路径 (key path) 的原生支持。KVO 在观察键路径 (key path) 时依赖于 NSKeyValueObservingOld 属性。

咱们也要指出有些集合是不能被观察的。KVO 旨在观察关系 (relationship) 而不是集合。咱们不能观察 NSArray,咱们只能观察一个对象的属性——而这个属性有多是 NSArray。举例说,若是咱们有一个 ContactList 对象,咱们能够观察它的 contacts 属性。可是咱们不能向要观察对象的 -addObserver:forKeyPath:... 传入一个 NSArray

类似地,观察 self 不是永远都生效的。并且这不是一个好的设计。

调试 KVO

你能够在 lldb 里查看一个被观察对象的全部观察信息。

(lldb) po [observedObject observationInfo]
复制代码

这会打印出有关谁观察谁之类的不少信息。

这个信息的格式不是公开的,咱们不能让任何东西依赖它,由于苹果随时均可以改变它。不过这是一个很强大的排错工具。

键值验证 (KVV)

最后提示,KVV 也是 KVC API 的一部分。这是一个用来验证属性值的 API,只是它光靠本身很难提供逻辑和功能。

若是咱们写可以验证值的 model 类的话,咱们就应该实现 KVV 的 API 来保证一致性。用 KVV 验证 model 类的值是 Cocoa 的惯例。

让咱们在一次强调一下:KVC 不会作任何的验证,也不会调用任何 KVV 的方法。那是你的控制器须要作的事情。经过 KVV 实现你本身的验证方法会保证它们的一致性。

如下是一个简单的例子:

- (IBAction)nameFieldEditingDidEnd:(UITextField *)sender;
{
    NSString *name = [sender text];
    NSError *error = nil;
    if ([self.contact validateName:&name error:&error]) {
        self.contact.name = name;
    } else {
        // Present the error to the user
    }
    sender.text = self.contact.name;
}
复制代码

它强大之处在于,当 model 类(Contact)验证 name 的时候,会有机会去处理名字。

若是咱们想让名字不要有先后的空白字符,咱们应该把这些逻辑放在 model 对象里面。Contact 类能够像这样实现 KVV:

- (BOOL)validateName:(NSString **)nameP error:(NSError * __autoreleasing *)error
{
    if (*nameP == nil) {
        *nameP = @"";
        return YES;
    } else {
        *nameP = [*nameP stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
        return YES;
    }
}
复制代码

通信录示例 里的 DetailViewControllerContact 类详解了这个用法。


原文 Key-Value Coding and Observing

相关文章
相关标签/搜索