KVC在iOS中的定义
无论是Swift还是Objective-C,KVC的定义都是对NSObject的扩展来实现的(Objective-c中有个显式的NSKeyValueCoding
类别名,而Swift没有,也不需要)所以对于所有继承了NSObject的类型,都能使用KVC(一些纯Swift类和结构体是不支持KVC的),下面是KVC最为重要的四个方法
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError;
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKeyPath:(NSString *)keyPath API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
同时苹果对一些容器类比如NSArray或者NSSet等,KVC有着特殊的实现。建议有基础的或者英文好的开发者直接去看苹果的官方文档,相信你会对KVC的理解更上一个台阶。(本人一直认为英文文档太繁琐,一直没有看过,有空看看)
KVC是怎么寻找Key的
KVC在内部是按什么样的顺序来寻找key的?
当调用setValue:属性值 forKey:@”name“
的代码时,底层的执行机制如下:
- 程序优先调用
set:属性值
方法,代码通过setter方法完成设置。注意,这里的是指成员变量名,首字母大清写要符合KVC的全名规则,下同 - 如果没有找到setName:方法,KVC机制会检查
+ (BOOL)accessInstanceVariablesDirectly
方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUNdefinedKey:
方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为_
的成员变量,无论该变量是在类接口部分定义,还是在类实现部分定义,也无论用了什么样的访问修饰符,只在存在以_
命名的变量,KVC都可以对该成员变量赋值。 - 如果该类即没有
set:
方法,也没有_
成员变量,KVC机制会搜索_is
的成员变量, - 和上面一样,如果该类即没有
set:
方法,也没有_
和_is
成员变量,KVC机制再会继续搜索和
is
的成员变量。再给它们赋值。 - 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的
setValue:forUNdefinedKey:
方法,默认是抛出异常。
如果开发者想让这个类禁用KVC里,那么重写+ (BOOL)accessInstanceVariablesDirectly
方法让其返回NO即可,这样的话如果KVC没有找到set:
属性名时,会直接用setValue:forUNdefinedKey:
方法。
在KVC中使用KeyPath
然而在开发过程中,一个类的成员变量有可能是其他的自定义类,你可以先用KVC获取出来再该属性,然后再次用KVC来获取这个自定义类的属性,但这样是比较繁琐的,对此,KVC提供了一个解决方案,那就是键路径KeyPath。
1
2
|
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
|
KVC如何处理异常
KVC中最常见的异常就是不小心使用了错误的Key,或者在设值中不小心传递了nil的值,KVC中有专门的方法来处理这些异常。
通常在用KVC操作Model时,抛出异常的那两个方法是需要重写的。虽然一般很小出现传递了错误的Key值这种情况,但是如果不小心出现了,直接抛出异常让APP崩溃显然是不合理的。
一般在这里直接让这个Key打印出来即可,或者有些特殊情况需要特殊处理。
通常情况下,KVC不允许你要在调用setValue:属性值 forKey:@”name“
(或者keyPath)时对非对象传递一个nil的值。很简单,因为值类型是不能为nil的。如果你不小心传了,KVC会调用setNilValueForKey:
方法。这个方法默认是抛出异常,所以一般而言最好还是重写这个方法。
1
2
|
[people1 setValue:nil forKey:@"age"]
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[ setNilValueForKey]: could not set nil as the value for the key age.' // 调用setNilValueForKey抛出异常
|
如果重写setNilValueForKey:
就没问题了
1
2
3
4
5
6
7
8
9
|
@implementation People
-(void)setNilValueForKey:(NSString *)key{
NSLog(@"不能将%@设成nil",key);
}
@end
//打印出
2016-04-17 16:19:55.298 KVCDemo[1304:92472] 不能将age设成nil
|
KVC的内部实现机制
前面我们对析了KVC是怎么搜索key的。所以如果明白了key的搜索顺序,是可以自己写代码实现KVC的。在考虑到集合和keyPath的情况下,KVC的实现会比较复杂,我们只写代码实现最普通的取值和设值即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
@interface NSObject(MYKVC)
-(void)setMyValue:(id)value forKey:(NSString*)key;
-(id)myValueforKey:(NSString*)key;
@end
@implementation NSObject(MYKVC)
-(void)setMyValue:(id)value forKey:(NSString *)key{
if (key == nil || key.length == 0) {
return;
}
if ([value isKindOfClass:[NSNull class]]) {
[self setNilValueForKey:key]; //如果需要完全自定义,那么这里需要写一个setMyNilValueForKey,但是必要性不是很大,就省略了
return;
}
if (![value isKindOfClass:[NSObject class]]) {
@throw @"must be s NSobject type";
return;
}
NSString* funcName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
if ([self respondsToSelector:NSSelectorFromString(funcName)]) {
[self performSelector:NSSelectorFromString(funcName) withObject:value];
return;
}
unsigned int count;
BOOL flag = false;
Ivar* vars = class_copyIvarList([self class], &count);
for (NSInteger i = 0; i
|
上面就是自己写代码实现KVC的部分功能。其中我省略了自定义KVC错误方法,省略了部分KVC搜索key的步骤,但是逻辑是很清晰明了的,后面的测试也符合预期。当然这只是我自己实现KVC的思路,Apple也许并不是这么做的。
KVC的使用
KVC在iOS开发中是绝不可少的利器,这种基于运行时的编程方式极大地提高了灵活性,简化了代码,甚至实现很多难以想像的功能,KVC也是许多iOS开发黑魔法的基础。下面我来列举iOS开发中KVC的使用场景
动态地取值和设值
利用KVC动态的取值和设值是最基本的用途了。相信每一个iOS开发者都能熟练掌握,
用KVC来访问和修改私有变量
对于类里的私有属性,Objective-C是无法直接访问的,但是KVC是可以的,请参考本文前面的Dog类的例子。
Model和字典转换
这是KVC强大作用的又一次体现,请参考我写的iOS开发技巧系列—打造强大的BaseMod系列文章,里面
充分地运用了KVC和Objc的runtime组合的技巧,只用了短短数行代码就是完成了很多功能。
修改一些控件的内部属性
这也是iOS开发中必不可少的小技巧。众所周知很多UI控件都由很多内部UI控件组合而成的,但是Apple度没有提供这访问这些空间的API,这样我们就无法正常地访问和修改这些控件的样式。而KVC在大多数情况可下可以解决这个问题。最常用的就是个性化UITextField中的placeHolderText了。
下面演示如果修改placeHolder的文字样式。这里的关键点是如果获取你要修改的样式的属性名,也就是key或者keyPath名。
一般情况下可以运用runtime来获取Apple不想开放的属性名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
let count:UnsafeMutablePointer = UnsafeMutablePointer()
var properties = class_copyIvarList(UITextField.self, count)
while properties.memory.debugDescription != "0x0000000000000000"{
let t = ivar_getName(properties.memory)
let n = NSString(CString: t, encoding: NSUTF8StringEncoding)
print(n) //打印出所有属性,这里我用了Swift语言
properties = properties.successor()
}
//上面省略了部分属性
Optional(_disabledBackgroundView)
Optional(_systemBackgroundView)
Optional(_floatingContentView)
Optional(_contentBackdropView)
Optional(_fieldEditorBackgroundView)
Optional(_fieldEditorEffectView)
Optional(_displayLabel)
Optional(_placeholderLabel) //这个正是我想要修改的属性。
Optional(_dictationLabel)
Optional(_suffixLabel)
Optional(_prefixLabel)
Optional(_iconView)
//下面省略了部分属性
|
可以从里面看到其他还有很多东西可以修改,运用KVC设值可以获得自己想要的效果。
操作集合
Apple对KVC的valueForKey:
方法作了一些特殊的实现,比如说NSArray和NSSet这样的容器类就实现了这些方法。所以可以用KVC很方便地操作集合
用KVC实现高阶消息传递
当对容器类使用KVC时,valueForKey:
将会被传递给容器中的每一个对象,而不是容器本身进行操作。结果会被添加进返回的容器中,这样,开发者可以很方便的操作集合来返回另一个集合。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
NSArray* arrStr = @[@"english",@"franch",@"chinese"];
NSArray* arrCapStr = [arrStr valueForKey:@"capitalizedString"];
for (NSString* str in arrCapStr) {
NSLog(@"%@",str);
}
NSArray* arrCapStrLength = [arrStr valueForKeyPath:@"capitalizedString.length"];
for (NSNumber* length in arrCapStrLength) {
NSLog(@"%ld",(long)length.integerValue);
}
打印结果
2016-04-20 16:29:14.239 KVCDemo[1356:118667] English
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Franch
2016-04-20 16:29:14.240 KVCDemo[1356:118667] Chinese
2016-04-20 16:29:14.240 KVCDemo[1356:118667] 7
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 6
2016-04-20 16:29:14.241 KVCDemo[1356:118667] 7
|
方法capitalizedString
被传递到NSArray中的每一项,这样,NSArray的每一员都会执行capitalizedString
并返回一个包含结果的新的NSArray。从打印结果可以看出,所有String都成功以转成了大写。
同样如果要执行多个方法也可以用valueForKeyPath:
方法。它先会对每一个成员调用 capitalizedString
方法,然后再调用length,因为lenth方法返回是一个数字,所以返回结果以NSNumber的形式保存在新数组里。
用KVC中的函数操作集合
KVC同时还提供了很复杂的函数,主要有下面这些
①简单集合运算符
简单集合运算符共有@avg, @count , @max , @min ,@sum5
种,都表示啥不用我说了吧, 目前还不支持自定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
@interface Book : NSObject
@property (nonatomic,copy) NSString* name;
@property (nonatomic,assign) CGFloat price;
@end
@implementation Book
@end
Book *book1 = [Book new];
book1.name = @"The Great Gastby";
book1.price = 22;
Book *book2 = [Book new];
book2.name = @"Time History";
book2.price = 12;
Book *book3 = [Book new];
book3.name = @"Wrong Hole";
book3.price = 111;
Book *book4 = [Book new];
book4.name = @"Wrong Hole";
book4.price = 111;
NSArray* arrBooks = @[book1,book2,book3,book4];
NSNumber* sum = [arrBooks valueForKeyPath:@"@sum.price"];
NSLog(@"sum:%f",sum.floatValue);
NSNumber* avg = [arrBooks valueForKeyPath:@"@avg.price"];
NSLog(@"avg:%f",avg.floatValue);
NSNumber* count = [arrBooks valueForKeyPath:@"@count"];
NSLog(@"count:%f",count.floatValue);
NSNumber* min = [arrBooks valueForKeyPath:@"@min.price"];
NSLog(@"min:%f",min.floatValue);
NSNumber* max = [arrBooks valueForKeyPath:@"@max.price"];
NSLog(@"max:%f",max.floatValue);
打印结果
2016-04-20 16:45:54.696 KVCDemo[1484:127089] sum:256.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] avg:64.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] count:4.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] min:12.000000
2016-04-20 16:45:54.697 KVCDemo[1484:127089] max:111.000000
|
②对象运算符
比集合运算符稍微复杂,能以数组的方式返回指定的内容,一共有两种:@distinctUnionOfObjects
@unionOfObjects
它们的返回值都是NSArray,区别是前者返回的元素都是唯一的,是去重以后的结果;后者返回的元素是全集。
用法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
NSLog(@"distinctUnionOfObjects");
NSArray* arrDistinct = [arrBooks valueForKeyPath:@"@distinctUnionOfObjects.price"];
for (NSNumber *price in arrDistinct) {
NSLog(@"%f",price.floatValue);
}
NSLog(@"unionOfObjects");
NSArray* arrUnion = [arrBooks valueForKeyPath:@"@unionOfObjects.price"];
for (NSNumber *price in arrUnion) {
NSLog(@"%f",price.floatValue);
}
2016-04-20 16:47:34.490 KVCDemo[1522:128840] distinctUnionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] unionOfObjects
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 22.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 12.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
2016-04-20 16:47:34.490 KVCDemo[1522:128840] 111.000000
|
前者会将重复的价格去除后返回所有价格,后者直接返回所有的图书价格。(因为只返回价格,没有返回图书,感觉用处不大。)
③Array和Set操作符
这种情况更复杂了,说的是集合中包含集合的情况,我们执行了如下的一段代码:
@distinctUnionOfArrays
@unionOfArrays
@distinctUnionOfSets@distinctUnionOfArrays:
该操作会返回一个数组,这个数组包含不同的对象,不同的对象是在从关键路径到操作器右边的被指定的属性里@unionOfArrays
该操作会返回一个数组,这个数组包含的对象是在从关键路径到操作器右边的被指定的属性里和@distinctUnionOfArrays不一样,重复的对象不会被移除@distinctUnionOfSets
和@distinctUnionOfArrays
类似。因为Set本身就不支持重复。
============================================kvo=======kvo=============================================================================================================
Key-Value Observing (KVO) 建立在 KVC 之上,它能够观察一个对象的 KVC key path 值的变化。
KVO的使用也很简单,就是简单的3步。
2.实现observeValueForKeyPath:ofObject:change:context:方法,这个方法当观察的属性变化时会自动调用
3.取消注册观察removeObserver:forKeyPath:context:
- textField.placeholder = @"username is in here!";
- [textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor"];
- [textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];
这里是使用了KVC的方式
参考链接
http://www.cnblogs.com/isItOk/p/5791644.html
http://www.cnblogs.com/isItOk/p/5651904.html
http://www.cnblogs.com/isItOk/p/5791638.html
http://www.cnblogs.com/isItOk/p/5651898.html
http://www.cnblogs.com/isItOk/p/6582859.html