KVC与NSKeyValueCoding

KVC全称是Key Value Coding,在NSKeyValueCoding.h非正式协议文件中,声明了KVC能使用的方法,KVC提供一种通过字符串来访问类中的属性和成员变量的方法。

  • NSKeyValueCoding协议中的方法


    NSKeyValueCoding.gif

从NSKeyValueCoding协议方法说去

先来看看几个宏
当调用KVC时key值为空时,就会抛出这个异常。
FOUNDATION_EXPORT NSExceptionName const NSUndefinedKeyException;

// NSKeyValueCoding中的运算符
NSKeyValueOperator const NSAverageKeyValueOperator; // 求平均值
NSKeyValueOperator const NSCountKeyValueOperator; // 统计总数
NSKeyValueOperator const NSDistinctUnionOfArraysKeyValueOperator; // 获取嵌套数组中不同的值
NSKeyValueOperator const NSDistinctUnionOfObjectsKeyValueOperator; // 获取不同的值
NSKeyValueOperator const NSDistinctUnionOfSetsKeyValueOperator; // 获取嵌套集合中不同的值
NSKeyValueOperator const NSMaximumKeyValueOperator; // 获取最大值
NSKeyValueOperator const NSMinimumKeyValueOperator; // 获取最小值
NSKeyValueOperator const NSSumKeyValueOperator; // 求和
NSKeyValueOperator const NSUnionOfArraysKeyValueOperator; // 获取嵌套数组中的值,不去重
NSKeyValueOperator const NSUnionOfObjectsKeyValueOperator; // 获取所有的值,不去重
NSKeyValueOperator const NSUnionOfSetsKeyValueOperator; // 获取嵌套集合中的值,不去重

这些运算符能帮助我们快速的运算集合中的操作。在官方文档中教了我们怎么使用这些操作符。

NSArray *array = @[@"1",@"1",@"1",@"2",@"2",@"3",@"3",@"4",@"5",@"5"];
    NSArray *array1 = @[@"3",@"4",@"5",@"5",@"6",@"6",@"7",@"8",@"8"];
    NSArray *unionArray = @[array,array1];
  • 获取数组的个数,这个一般没有,因为数组就要count属性。
    NSString *count = [array valueForKeyPath:@"@count"];
    NSLog(@"数组个数 = %@",count);
2018-06-19 15:50:20.568834+0800 KVC和NSKeyValueCoding[10417:285376] 数组个数 = 10
  • 获取数组的平均值
    NSString *avg = [array valueForKeyPath:@"@avg.self"];
    NSLog(@"数组平均值 = %@",avg);
2018-06-19 15:50:20.569342+0800 KVC和NSKeyValueCoding[10417:285376] 数组平均值 = 2.7
  • 获取去重后的数组。如果有一道题目是让我们从一万个重复的数据中找出不同的数,就可以用这个方法。当然也可以数组转NSSet去重,再转NSArray排序。
    NSArray *distinct = [array valueForKeyPath:@"@distinctUnionOfObjects.self"];
    NSArray *distinctUnionOfObjects = [distinct sortedArrayUsingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        return [obj1 compare:obj2];
    }]; //数组正序
    NSLog(@"去重后的数组 = %@",distinctUnionOfObjects);
2018-06-19 15:50:20.569594+0800 KVC和NSKeyValueCoding[10417:285376] 去重后的数组 = (
    1,
    2,
    3,
    4,
    5
)
  • 数组数字最大值
    NSString *max = [array valueForKeyPath:@"@max.self"];
    NSLog(@"数组数字最大值 = %@",max);
2018-06-19 15:50:20.569777+0800 KVC和NSKeyValueCoding[10417:285376] 数组数字最大值 = 5
  • 数组数字最小值
    NSString *min = [array valueForKeyPath:@"@min.self"];
    NSLog(@"数组数字最小值 = %@",min);
2018-06-19 15:50:20.569928+0800 KVC和NSKeyValueCoding[10417:285376] 数组数字最小值 = 1
  • 获取数组数字总和
    NSString *sum = [array valueForKeyPath:@"@sum.self"];
    NSLog(@"总和  = %@", sum);
2018-06-19 15:50:20.570135+0800 KVC和NSKeyValueCoding[10417:285376] 总和  = 27
  • 不去重的数组,这个在开发中也没用,因为就算数组本身。
    NSArray *unionOfObjects = [array valueForKeyPath:@"@unionOfObjects.self"];
    NSLog(@"不去重的数组 = %@", unionOfObjects);
2018-06-19 15:50:20.570313+0800 KVC和NSKeyValueCoding[10417:285376] 不去重的数组 = (
    1,
    1,
    1,
    2,
    2,
    3,
    3,
    4,
    5,
    5
)
  • 多个数组去重。去重后变一个数组。
    NSArray *distinctUnionOfArrays = [unionArray valueForKeyPath:@"@distinctUnionOfArrays.self"];
    NSLog(@"多个数组去重后 = %@",distinctUnionOfArrays);
2018-06-19 15:50:20.570545+0800 KVC和NSKeyValueCoding[10417:285376] 多个数组去重后 = (
    2,
    3,
    4,
    5,
    6,
    7,
    8,
    1
)
  • 多个数组不去重,这个在开发中被可变数组的addObjectsFromArray方法替代.
    NSArray *unionOfArrays = [unionArray valueForKeyPath:@"@unionOfArrays.self"];
    NSLog(@"多个数组不去重 = %@",unionOfArrays);
2018-06-19 15:50:20.570716+0800 KVC和NSKeyValueCoding[10417:285376] 多个数组不去重 = (
    1,
    1,
    1,
    2,
    2,
    3,
    3,
    4,
    5,
    5,
    3,
    4,
    5,
    5,
    6,
    6,
    7,
    8,
    8
)
赋值和取值操作
  • 创建一个 Animal 自定义对象
#import <Foundation/Foundation.h>

@class  Food;
/**
 对象
 */
@interface Animal : NSObject

 // 动物姓名
@property (nonatomic, copy) NSString *name;
 // 动物年龄 
@property (nonatomic, assign) NSInteger age;
 // 食物 
@property (nonatomic, strong) Food *food;

@end

@interface Food : NSObject
 // 水果
@property (nonatomic, copy) NSString *fruit;
 // 肉 
@property (nonatomic, copy) NSString *meat;

@end
#import "Animal.h"

@implementation Animal

- (Food *)food {
    if (!_food) {
        _food = [[Food alloc] init];
    }
    return _food;
}
@end


@implementation Food

@end
  • 使用- setValue: forKey: 赋值,valueForKey:方法取值。直接通过Animal中的属性名来作为key赋值取值。
[_animal setValue:@"小妹" forKey:@"name"];
NSLog(@"name = %@",[_animal valueForKey:@"name"]);
2018-06-19 17:13:32.029108+0800 KVC和NSKeyValueCoding[11681:325413] name = 小妹
  • 如果想对Animal对象中的Food对象的fruit属性赋值,这种多级访问设置或获取value就要用到keyPath来存取。
 [_animal setValue:@"苹果" forKeyPath:@"food.fruit"];
 NSLog(@"fruit = %@",[_animal valueForKeyPath:@"food.fruit"]);
2018-06-19 17:34:33.650983+0800 KVC和NSKeyValueCoding[11746:338536] fruit = 苹果

如果用- setValue: forKey:存取,则会造成crash

[_animal setValue:@"苹果" forKey:@"food.fruit"];
NSLog(@"fruit = %@",[_animal valueForKey:@"food.fruit"]);
Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Animal 0x60000003cdc0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key food.fruit.'
多值的赋值和取值
  • 使用的方法
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *, id> *)keyedValues;
  • 例子
    [_animal setValuesForKeysWithDictionary:@{@"name":@"小马哥" , @"age" : @(25)}];
    NSLog(@"name = %@",[_animal valueForKey:@"name"]);
    NSLog(@"age = %li",_animal.age);
    NSLog(@"赋值后为 %@",[_animal dictionaryWithValuesForKeys:@[@"name",@"age"]]);
  • 方法输出
2018-06-20 09:19:00.728542+0800 KVC和NSKeyValueCoding[12597:503862] name = 小马哥
2018-06-20 09:19:00.728667+0800 KVC和NSKeyValueCoding[12597:503862] age = 25
2018-06-20 09:19:00.728941+0800 KVC和NSKeyValueCoding[12597:503862] 赋值后为 {
    age = 25;
    name = "\U5c0f\U9a6c\U54e5";
}
  • 项目中,我们字典转模型通常是
 // 初始化赋值
- (instancetype)initWithDic:(NSDictionary *)dic {
    if (self = [super init]) {
         self.age = [dic[@"age"] integerValue];
         self.name = dic[@"name"];
    }
    return self;
}

但是当我们知道了多值操作的方法,并且model的属性和字典的key相同,我们可以使用下面方法,减少赋值的多余代码。

 // 初始化赋值
- (instancetype)initWithDic:(NSDictionary *)dic {
    if (self = [super init]) {
        [self setValuesForKeysWithDictionary:dic];
    }
    return self;
}
// 重写 setValuesForKeysWithDictionary方法 去除 nil和null
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues {
    for (id key in keyedValues.allKeys) {
        if (![keyedValues valueForKey:key]) {
            [self setValue:@"" forKey:key];
        } else if ([[keyedValues valueForKey:key] isEqual:[NSNull null]]) {
            [self setValue:@"" forKey:key];
        } else {
            [self setValue:[keyedValues valueForKey:key] forKey:key];
        }
    }
}
集合getter

在对集合对象进行getter操作时,可以调用一下方法通过key或者keyPath获取。如果再对容器类进行addremove等操作时,就会触发KVO的消息通知

  • key
- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
- (NSMutableOrderedSet *)mutableOrderedSetValueForKey:(NSString *)key API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
- (NSMutableSet *)mutableSetValueForKey:(NSString *)key;
  • keypath
- (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;
属性验证
  • 相对应的也有key和keyPath对应的两个方法。用来验证传入的value和key是否正确。
- (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;
 // 验证码`key value` 是否满足需求
- (BOOL)validateValue:(inout id  _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError * _Nullable __autoreleasing *)outError {
    if ([inKey isEqualToString:@"age"]) {
        if ([*ioValue integerValue] < 18) {
            *outError = [NSError errorWithDomain:AnimalErrorDomain code:10001 userInfo:@{NSLocalizedDescriptionKey : @"小动物还未成年"}];
            return NO;
        } 
    }
    return YES;
}
NSNumber *age = @(12);
    NSError *error;
    if ([_animal validateValue:&age forKey:@"age" error:&error]) {
        NSLog(@"小动物成年了");
    } else {
        NSLog(@"error = %@",error);
    }
2018-06-20 09:07:57.458669+0800 KVC和NSKeyValueCoding[12550:497482] error = Error Domain=AnimalErrorDomain Code=10001 "小动物还未成年" UserInfo={NSLocalizedDescription=小动物还未成年}

KVC的底层实现

为了方便阅读,将实例变量放在了.h文件中。

  • 赋值流程
    1 先调用属性的setter方法完成赋值。
    2 如果没有发现setter方法,则检查+ (BOOL)accessInstanceVariablesDirectly方法,默认返回YES,这是在没有找到存取器的时候才调用的方法。返回为YES表示查找_<key>_is<Key><key>is<Key>value,如果有则赋值。
    3 如果都没有找到,则会调用setValue:forUndefinedKey:方法并抛出异常。

  • 测试
    .h文件

#import <Foundation/Foundation.h>

@class  Food;
/**
 对象
 */
@interface Animal : NSObject {
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
}

 // 动物姓名
@property (nonatomic, copy) NSString *name;

@end

.m文件

#pragma mark --- KVC的底层实现测试
 // 是否可以按照 _<key>、_is<Key>、<key>、is<Key> 方式查找
+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

 // setter
- (void)setName:(NSString *)name {
     NSLog(@"调用了setter方法");
    _name = name;
}

 // 重写setValue: forKey: 用来查看当前的key。具体是_name,_isName,name,isName.
- (void)setValue:(id)value forKey:(NSString *)key {
    NSLog(@"%@", [NSString stringWithFormat:@"当前调用的key %@",key]);
    [super setValue:value forKey:key];
}
    // kvc底层实现验证
    Animal *animal = [[Animal alloc] init];
    [animal setValue:@"晓东" forKey:@"name"];
    NSLog(@"name = %@",[animal valueForKey:@"name"]);

打印

2018-06-20 13:38:45.541011+0800 KVC和NSKeyValueCoding[14274:650523] 当前调用的key name
2018-06-20 13:38:45.541134+0800 KVC和NSKeyValueCoding[14274:650523] 调用了setter方法
2018-06-20 13:38:45.541251+0800 KVC和NSKeyValueCoding[14274:650523] name = 晓东

现在把属性name去掉,这样就不会默认生成存取器方法。因为accessInstanceVariablesDirectly为NO,所有会走setValue:(id)value forUndefinedKey:(NSString *)key方法。我们如果不想让外界访问类的成员变量,则可以将accessInstanceVariablesDirectly属性赋值为NO。

@interface Animal : NSObject {
    NSString *name;
    NSString *_name;
    NSString *_isName;
    NSString *isName;
}

 // 动物姓名
//@property (nonatomic, copy) NSString *name;
#pragma mark --- KVC的底层实现测试
 // 是否可以按照 _<key>、_is<Key>、<key>、is<Key> 方式查找
+ (BOOL)accessInstanceVariablesDirectly {
    return NO;
}

 // setter
//- (void)setName:(NSString *)name {
//    NSLog(@"调用了setter方法");
//    _name = name;
//}

 // 重写setValue: forKey: 用来查看当前的key。具体是_name,_isName,name,isName.
- (void)setValue:(id)value forKey:(NSString *)key {
    NSLog(@"%@", [NSString stringWithFormat:@"当前调用的key %@",key]);
    [super setValue:value forKey:key];
}
2018-06-20 13:49:54.816830+0800 KVC和NSKeyValueCoding[14428:661962] 当前调用的key name
2018-06-20 13:49:54.823548+0800 KVC和NSKeyValueCoding[14428:661962] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Animal 0x604000277500> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'

接下来把accessInstanceVariablesDirectly变成YES,因为存在_name,_isName,name,isName四个实例变量,所以会找到相应的key并将value赋值。依次注释操作

@interface Animal : NSObject {
    NSString *name;
//    NSString *_name;
//    NSString *_isName;
//    NSString *isName;
}
@interface Animal : NSObject {
//    NSString *name;
    NSString *_name;
//    NSString *_isName;
//    NSString *isName;
}
@interface Animal : NSObject {
//    NSString *name;
//    NSString *_name;
    NSString *_isName;
//    NSString *isName;
}
@interface Animal : NSObject {
//    NSString *name;
//    NSString *_name;
//    NSString *_isName;
    NSString *isName;
}

打印出来的都是

2018-06-20 13:55:08.241934+0800 KVC和NSKeyValueCoding[14468:666416] 当前调用的key name
2018-06-20 13:56:40.147144+0800 KVC和NSKeyValueCoding[14497:668042] name = 晓东

当实例变量_name,_isName,name,isName都不存在时并且setter方法不存在,

NSKeyValueCoding[14428:661962] 当前调用的key name
2018-06-20 13:49:54.823548+0800 KVC和NSKeyValueCoding[14428:661962] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Animal 0x604000277500> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key name.'

错误处理

  • key值的错误
    上面一直报的一个异常setValue:forUndefinedKey:,我们可以通过重写这个方法来停止报这个异常。
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
    NSLog(@"赋值失败,该key不存在%@",key);
}

重写后打印。

2018-06-20 14:05:23.053180+0800 KVC和NSKeyValueCoding[14535:674635] 赋值失败,该key不存在name
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容

  • TST口红——[em]e400591[/em]蜜桃粉口红——[em]e400591[/em]樱花粉口红——[em]...
    smile_14fa阅读 145评论 0 0
  • 愿望,这两个字包括很多。相信每个人都有自己的愿望,而且也会有相似之处。 在我的记忆长河中,随着年龄的增长我的愿望在...
    怀旧的星空阅读 330评论 0 1
  • 在一月的最后一天给导师看了论文二稿,在二月的第一天回到了家。回顾了一下自己在2017年末定下的2018年1...
    亲爱的戈多在哪阅读 477评论 0 1
  • 我想做一朵野花 肆意的长在无尽的原野 扎根在厚厚的泥土里 面朝太阳 沐浴着温暖的阳光 我如同所有野生的植物一般 热...
    陆上粱阅读 504评论 0 0