今天整理了一下有关KVC的知识,参考苹果文档
KVC的基本用法
访问对象的属性
赋值
这里有三种类型。属性,一对一关系,一对多关系。如下
@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance; // 属性
@property (nonatomic) Person* owner; // 一对一
@property (nonatomic) NSArray< Person* >* transferPerson; // 一对多
@end
分别用KVC给三个属性赋值
- 直接的属性可以用setValue:forKey:
- 如果属性对象包含的属性(比如设置owner.name),可以用setValue:forKeyPath: 3)setValuesForKeysWithDictionary:。通过传递的字典,遍历所有的keys,调用setValue:forKey:来给对象赋值。 代码如下:
//设置利率
[account setValue:@(4.5) forKey:@"currentBalance"];
//设置户主的名字(Person类里面有name属性)
[account setValue:@"Jack" forKeyPath:@"owner.name"];
//设置户主家所在的城市(Person类里面有个Address *address)
[account setValue:@"Beijing" forKeyPath:@"owner.address.city"];
//设置户主所有转账人的名字。
[account setValue:@"transferPerson" forKeyPath:@"transferPerson.name"];
//通过字典给account赋值
Person *person = [Person new];
person.name = @"rose";
[account setValuesForKeysWithDictionary:@{@"currentBalance":@(4.5),@"owner":person}];
取值
- valueForKey: 和 valueForKeyPath: 来取值。
- dictionaryWithValuesForKeys: 这个方法会遍历传进来的keys数组,调用valueForKey:,得到的value和key添加到字典中,最终返回此字典。 代码如下:
//取利率
NSNumber *currentBalance = [account valueForKey:@"currentBalance"];
//取户主的姓名
NSString *name = [account valueForKeyPath:@"owner.name"];
//取户主的所有转账人的名字
NSArray *transferNames = [account valueForKeyPath:@"transferPerson.name"];
//通过一个包含放key的数组,得到key:value字典。比如此值为{@"currentBalance":@(4.5)}
NSDictionary *transferDic = [account dictionaryWithValuesForKeys:@[@"currentBalance"]];
此外,如果想要操作一些集合类属性的内容,需要返回一可变类型的,可用以下方法
- (NSMutableArray有序数组)mutableArrayValueForKey: 和 mutableArrayValueForKeyPath:
- (NSMutableSet无序集合、元素不重复)mutableSetValueForKey: 和 mutableSetValueForKeyPath:
- (NSMutableOrderedSet有序集合,元素不重复)mutableOrderedSetValueForKey: 和 mutableOrderedSetValueForKeyPath:
使用运算符
集合运算符
- 取平均数(@avg)、求各(@sum)
//计算转账人的平均年龄。如果有3个转账人,年龄分别是11,22,33
NSNumber *avgAge = [account.transferPerson valueForKeyPath:@"@avg.age"];//得出22
//年龄和
NSNumber *sumAge = [account.transferPerson valueForKeyPath:@"@sum.age"];//得出66
先获取集合里面的每一元素,转成double类型(我试了字符串会崩溃,应该是包含NSNumber类型和CGFloat这种类型。nil类型会转成0),然后计算平均值或者和。结果会被存在NSNumber里面返回。 2) 获取集合的个数(@count)
//计算转账人的个数。即返回数组的个数3。
NSNumber *count = [account.transferPerson valueForKeyPath:@"@count"];
- 获取集合中的最大值(@max)和最小值(@min)
//获取最大值
NSNumber *maxAge = [account.transferPerson valueForKeyPath:@"@max.age"];
//获取最小值
NSNumber *minAge = [account.transferPerson valueForKeyPath:@"@min.age"];
通过compare:来比较。且会忽略掉nil。即如果有一转账的人没有年龄,minAge不为0,而是返回已知年龄里的最小值。 4)
数组运算符
- 删除数组中重复的值(@distinctUnionOfObjects)
//将上面的转账人年龄改成22,22,33,则ageArr返回@[22, 33]
NSArray *ageArr = [account.transferPerson valueForKeyPath:@"@distinctUnionOfObjects.age"];
- 显示数组中所有的值(@unionOfObjects)
//这个和下面那个结果显示一样。
NSArray *ageArr = [account.transferPerson valueForKeyPath:@"@unionOfObjects.age"];
NSArray *ageArr = [account.transferPerson valueForKey:@"age"];
KVC访问搜索模式
通过valueForKey:来取值
执行步骤如下:
1、 先找可以访问到此实例的方法。
- 名字类似下面的方法,顺序如下:
get<Key> -> <key> -> is<Key> -> _<key>
这里的key是有大小写之分的,is和get开头的方法跟着的key是大写开头的,而key和_key是跟key一样的。也就是说会按以下顺序来寻找方法:
//如果key是name
getName: -> name: -> isName: -> _name:
//如果key是Name(当然一般命名都以小写字母开头)
getName: -> Name: -> isName: -> _Name:
//如果key是_name
get_name: -> _name: -> is_name: -> __name:
//如果key是_Name
get_Name: -> _Name: -> is_Name: -> __Name:
如果没找到,则继续按下面方法来查找方法。
- 查找名字类似下面的方法,以name为例:
官方文档上说是会返回一个可以响应NSArray所有方法的代理集合。按下面这种写法,返回的值是NSKeyValueArray类型的。
//countOf<Key> 和 (objectIn<Key>AtIndex:和<key>AtIndexes:的一种)。
//如果后面这两个方法都实现,则只走objectIn<Key>AtIndex:这个方法。
//返回这个数组的个数,此处默认写了2。
- (NSUInteger)countOfName {
return 2;
}
//遍历count次(本demo中是2次),返回每个index的值。即此处最终得到结果为@[@[@1,@2],@[@1,@2]]
- (id)objectInNameAtIndex:(NSInteger)index {
return @[@1,@2];
}
//也是遍历count次,不过取值和返回的值和数组的objectsAtIndexes:方法一样。结果是@[@1,@2]
- (NSArray *)currentNameAtIndexes:(NSIndexSet *)indexSet {
return [@[@1,@2] objectsAtIndexes:indexSet];
}
//这个方法可写可不写,能提高性能。
//它从集合中返回属于指定范围内的对象,并与NSArray方法getObjects:range:相对应。
//实践中发现如果写了这个方法,遍历count次的就不走了,否则那个遍历次数会被调用count次(本demo中就是2次)。
- (void)getName:(id __unsafe_unretained *)buffer
range:(NSRange)inRange {
[@[@1,@2] getObjects:buffer range:inRange];
}
如果没找到,则继续按下一方法来查找方法。
- 查找下面的方法,以name为例:
官方文档上说是返回一个可以响应NSSet所有方法的代理集合。按下面这种写法,返回的值是NSKeyValueSet类型的。
//countOf<Key> 和 enumeratorOf<Key> 和 memberOf<Key>: 三种方法都实现
//返回集合的个数。此处返回了固定值3。
- (NSUInteger)countOfCurrentBalance {
return 3;
}
//返回一个迭代器类型。不知道可以返回别的不、
- (id)enumeratorOfCurrentBalance {
return [@[@1,@3,@"我们"] objectEnumerator];
}
//这个也必须写。判断set里面是否有anObject,有的话返回该对象,没有返回nil。
- (id)memberOfEmployees:(id)anObject {
NSSet *set = [NSSet setWithArray:@[@1,@3,@"我们"]];
return [set member:anObject];
}
如果这些方法都没有找到,则执行下一步。
2、如果没找到方法,就找实例变量。
- 检查类方法accessInstanceVariablesDirectly 是否返回YES。默认是返回YES。
- 如果返回NO,则终止此查找。抛出valueForUndefinedKey异常(第3步)。
- 如果返回YES,则按下面顺序开始找。
_<key> -> _is<Key> -> <key> -> is<Key>
同上,这里key也是有大小写之分,也就是说会按以下顺序来寻找成员变量:
//如果key是name
_name -> _isName -> name -> isName
//如果key是Name(当然一般命名都以小写字母开头)
_Name -> _isName -> Name -> isName
//如果key是_name
__name -> _is_name -> _name -> is_name
//如果key是_Name
__Name -> _is_Name -> _Name -> is_Name
如果实例变量也没有找到,则会抛出异常也就是下步。
3、异常处理。
可以通过重写valueForUndefinedKey:方法,来处理异常。
- (id)valueForUndefinedKey:(NSString *)key {
//可以对某个key做单独处理
if ([key isEqualToString:@"Name"]) {
return _name;//_name为此类的一实例变量
return @"123";//固定字符串,如果没找到,通过valueForKey得到的Name永远是123。
}
return nil;
}
通过setValue:forKey:来赋值。
1、先找set方法,按以下顺序:
set<Key> -> _set<Key>
//比如name。则会调用setName: -> _setName:
如果找不到方法,执行下一步,找实例变量。
2、找实例变量
- 检查类方法accessInstanceVariablesDirectly 是否返回YES。默认是返回YES。
- 如果返回NO,则终止此查找。setValue:forUndefinedKey:(第3步)。
- 如果返回YES,则按下面顺序开始找。
_name -> _isName -> name -> isName
如果没找到,就会抛出异常,也就是下一步。
3、异常处理。
如果是没有找到上面的变量,则可以通过重写setValue:forUndefinedKey:方法解决。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
if ([key isEqualToString:@"name"]) {
//做一些处理
}
}
如果是设置了nil导致的崩溃(给非对象的属性赋值,比如BOOL类型的)。则可以通过重写setNilValueForKey:来处理。
- (void)setNilValueForKey:(NSString *)key {
if ([key isEqualToString:@"hidden"]) {
[self setValue:@(NO) forKey:@”hidden”];//设置为NO。
} else {
[super setNilValueForKey:key];
}
}
通过mutableArrayValueForKey:来取值等
//插入
insertObject:in<Key>AtIndex: or insert<Key>:atIndexes:
//移除
removeObjectFrom<Key>AtIndex: or remove<Key>AtIndexes:
//替换
replaceObjectIn<Key>AtIndex:withObject: or replace<Key>AtIndexes:with<Key>:
获取到的可变数组,通过insert、remove、replace方法可直接对数组操作,不需要改完再存一次。代码如下(以transactions为例,_transactions为成员变量):
- (void)insertObject:(NSNumber *)transaction
inTransactionsAtIndex:(NSUInteger)index {
[_transactions insertObject:transaction atIndex:index];
}
- (void)insertTransactions:(NSArray *)transactionArray
atIndexes:(NSIndexSet *)indexes {
[_transactions insertObjects:transactionArray atIndexes:indexes];
}
- (void)removeObjectFromTransactionsAtIndex:(NSUInteger)index {
[_transactions removeObjectAtIndex:index];
}
- (void)removeTransactionsAtIndexes:(NSIndexSet *)indexes {
[_transactions removeObjectsAtIndexes:indexes];
}
- (void)replaceObjectInTransactionsAtIndex:(NSUInteger)index
withObject:(id)anObject {
[_transactions replaceObjectAtIndex:index
withObject:anObject];
}
- (void)replaceTransactionsAtIndexes:(NSIndexSet *)indexes
withTransactions:(NSArray *)transactionArray {
[_transactions replaceObjectsAtIndexes:indexes
withObjects:transactionArray];
}
用法:
//获取account的成员变量transactions的值。
NSMutableArray *array = [account mutableArrayValueForKey:@"transactions"];
//在index=1处插入@3
[array insertObject:@3 atIndex:1];//此时account的transactions[1]已变成3。
KVC验证方法
格式如下:
- (BOOL)validate<Key>:(id *)ioValue error:(NSError * __autoreleasing *)outError
可用来验证属性的值是否合理。比如age,在Person类里面实现下面方法
- (BOOL)validateAge:(id *)ioValue error:(NSError * __autoreleasing *)outError {
if (*ioValue == nil) {
*ioValue = @(0);
} else if ([*ioValue floatValue] < 0.0) {
if (outError != NULL) {
*outError = [NSError errorWithDomain:@"PersonErrorDomain"
code:1011
userInfo:@{ NSLocalizedDescriptionKey
: @"年龄不能小于0" }];
}
return NO;
}
return YES;
}
验证的代码如下:
NSNumber *age = @(account.owner.age);
NSError* error;
BOOL result = [account validateValue:&(age) forKeyPath:@"owner.age" error:&error];
if (!result) {
NSLog(@"%@",error);
}