KVO - Key Value Observer

在MVC架构中保持Model和View的统一性

定义依赖属性

依赖属性可以用来表明属性之间的依赖关系,从而使得在set被依赖属性的时候,依赖属性也会收到通知。定义依赖属性有以下两种方式:

  • 为单个属性定义依赖属性
1
2
3
4
+ (NSSet<NSString *> *)keyPathsForValuesAffecting<#DependentKey#>
{
return [NSSet setWithObjects:@"<#keyPath#>", nil];
}
  • 为任意属性定义依赖属性
1
2
3
4
5
6
7
+ (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
if ([key isEqualToString:@"key1"]) {
return [NSSet setWithObjects:@"first", @"last", nil];
}
else if...
}

依赖属性的使用

在想要observe的属性上加上observer:

1
[_labColor addObserver:observer forKeyPath:@"key" options:someOptions context:someContext];

此处options主要有以下几种选项:

1
2
3
4
5
6
typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
NSKeyValueObservingOptionNew = 0x01,
NSKeyValueObservingOptionOld = 0x02,
NSKeyValueObservingOptionInitial = 0x04,
NSKeyValueObservingOptionPrior = 0x08
};

NSKeyValueObservingOptionNew表示传递新的赋值,NSKeyValueObservingOptionOld表示传递赋值前的值,NSKeyValueObservingOptionInitial表示在addObserve的时候就会调用一次下述函数,相当于进行一次初始化,NSKeyValueObservingOptionPrior则表示在该属性变更前后会各收到一次notifier,这些不同选项所传递的值都在change中。

注意该observer必须实现如下方法:

1
2
3
4
5
6
7
8
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == <#context#>) {
<#code to be executed upon observing keypath#>
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}

手动通知与自动通知

在被observe的对象上实现如下方法可以关闭自动notify:

1
2
3
4
+ (BOOL)automaticallyNotifiesObserversOfLComponent
{
return NO;
}

想要手动发送notifier,可以通过重写相应的属性的set方法,举例如下:

1
2
3
4
5
6
7
8
9
- (void)setLComponent:(double)lComponent
{
if (_lComponent == lComponent) {
return;
}
[self willChangeValueForKey:@"lComponent"];
_lComponent = lComponent;
[self didChangeValueForKey:@"lComponent"];
}

此处我们可以知道,在自动通知的情况下,willChangeValueForKey和didChangeValueForKey会自动调用。

此外,如果需要更加详细的信息,可以重写如下will方法和对应的did方法:

1
2
3
4
- (void)willChangeValueForKey:(NSString *)key;
- (void)willChange:(NSKeyValueChange)changeKind valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key;
// 只对mutable版本有效
- (void)willChangeValueForKey:(NSString *)key withSetMutation:(NSKeyValueSetMutationKind)mutationKind usingObjects:(NSSet *)objects;

然后手动调用will和did方法即可(记得关闭自动通知,否则会通知两次)。

对可变集合类的支持

由于在向可变集合类内添加元素的时候,该类对象的地址不一定会发生改变,从而就不一定会产生notifier通知给observer,因此在向可变集合类内添加元素的时候,可以调用如下函数:

1
2
3
-mutableArrayValueForKey:
-mutableSetValueForKey:
-mutableOrderedSetValueForKey:

举例:

1
2
3
4
5
6
7
被observe的类中有这个对象:
@property (nonatomic, strong) NSMutableArray *testArray;
如果要观察这个对象,添加observer的方法如下:
self.mutableArrayTest = [[MutableArrayTest alloc] init];
[self.mutableArrayTest addObserver:self forKeyPath:@"testArray" options:someOptions context:nil];
在更改testArray中的对象时,调用:
[[self.mutableArrayTest mutableArrayValueForKey:@"testArray"] addObject:@"111"];

KVC - Key Value Coding

设置属性,简化UI

可以直接通过如下方法修改对象中的属性值:

1
- (void)setValue:(id)value forKey:(NSString *)key;

使用这个方法就可以直接通过属性名在MVC中的Model层修改属性直接对应的UI。

也可以使用如下方法实现批量的设置:

1
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;

Key Path

以下两个函数可以通过类似属性的方式读写对象内部的属性:

1
2
- (void)setValue:(id)value forKeyPath:(NSString *)keyPath;
- (id)valueForKeyPath:(NSString *)keyPath;

举例如下:

1
2
3
4
// 修改当前类下labColor对象下的obj对象的testString属性值为@"WWWWWW"
[self setValue:@"WWWWWW" forKeyPath:@"labColor.obj.testString"];
// 获取_labColor对象下obj对象的testString属性的值
NSLog(@"%@", [_labColor valueForKeyPath:@"obj.testString"]);

不定义property但是可以设置属性

可以通过实现-和-set这两个函数来动态定义key属性,然后在访问和修改这个属性的时候则可以使用valueForKey和setValue:forKey方法。

如果想要使用setValue:forKey方法给key属性设置nil,则会抛出异常,可以通过重写以下方法来处理:

1
- (void)setNilValueForKey:(NSString *)key;

动态添加属性还有一种方法,就是重写以下方法,所有之前未定义过的key都会走这个方法兜底:

1
2
- (id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(id)value forUndefinedKey:(NSString *)key;

集合操作

参考https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueCoding/CollectionOperators.html

对于集合类,可以通过在valueForKeyPath方法中设置不同的key实现,key的一般格式如下:

keypath

如果对象本来就是集合类,则left key path可以省略。

举例:

1
2
// 返回self.labColor.testArr这个数组内的对象的amount属性的平均值
[self valueForKeyPath:@"labColor.testArr.@avg.amount"])

一般有三种collection operator

聚合操作算子 Aggregate Operator

@avg, @min, @max, @count, @sum

数组操作算子 Array Operator

@distinctUnionOfObjects, @unionOfObjects

嵌套操作算子 Nesting Operator

@distinctUnionOfArrays, @unionOfArrays, @distinctUnionOfSets

集合代理

首先举例说明:

如果我们在类中实现了如下方法:

1
2
3
4
5
6
7
8
9
10
11
- (NSUInteger)countOfContacts{
return 10;
}
- (id)objectInContactsAtIndex:(NSUInteger)idx{
if (idx < 5) {
return @"first half";
}
else {
return @"last half";
}
}

那么我们就可以通过[_labColor valueForKey:@“contacts”]来得到如下数组:

1
2
3
4
5
6
7
8
9
10
11
12
(
"first half",
"first half",
"first half",
"first half",
"first half",
"last half",
"last half",
"last half",
"last half",
"last half"
)

相当于动态的在该类中添加了一个集合对象(实际上这个对象的类型是NSKeyValueArray,其内部包含一个NSArray对象)。

类似的,我们也可以通过这种方式实现NSSet和NSOrderedSet:

array

同时也可以在上面这些方法的基础上再实现额外的添加和删除方法实现对应的mutable版本的集合类:

mutablearray

KVV - Key Value Validation

可以在调用KVC函数之前判断即将要赋的值是否合法,主要是下面两个函数:

1
2
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;

这两个函数会去调用对应的对象中的如下方法:

1
- (BOOL)validate<key>:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError;

如果没有重写这个方法,则默认返回YES,表示所有值都合法,可以通过重写这个函数实现KVV。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// LabColor.m
// 对于firstName这个属性,@“Japan”是不合法的值
- (BOOL)validateFirstName:(id *)value error:(out NSError * _Nullable __autoreleasing *)outError
{
NSString *firstName = *value;
if ([firstName isEqualToString:@"japan"]) {
return NO;
}
return YES;
}
// 使用
_labColor = [[LabColor alloc] init];
NSString *value1 = @"japan";
NSString *value2 = @"japann";
NSError *err;
NSLog(@"%d", [_labColor validateValue:&value1 forKey:@"firstName" error:&err]);
// 返回NO
NSLog(@"%d", [_labColor validateValue:&value2 forKey:@"firstName" error:&err]);
// 返回YES

KVC中的函数调用顺序

取值 valueForKey: 和 valueForKeyPath:

getter

  1. 直接get的四个函数方法;
  2. 是否实现了NSArray的方法;
  3. 是否实现了NSSet方法;
  4. accessInstanceVariablesDirectly返回YES并且存在对应的实例变量;
  5. 如果上述四步任意一步取得了值则在这一步进行包装;
  6. 否则调用valueForUndefinedKey方法抛出异常(可以重写该方法不抛出异常)。

设置值 setValue:forKey: 和 setValue:forKeyPath:

setter

  1. 首先查找是否有对应的set方法,如果有则直接调用;
  2. accessInstanceVariablesDirectly返回YES并且存在对应的实例变量,则直接设置实例变量即可;
  3. 调用setValue:forUndefinedKey:方法,默认是抛出异常,可以重写。

参考

KVC 和 KVO

iOS开发—图解KVC