教你使用runtime写一个简单的字典转模型的工具类
一.自动生成属性的分类
模型属性,通常需要跟字典中的key一一对应。从服务器得到的数据太杂?数据太多?写成plist文件后一个个对照填写属性,太繁琐?那么我么可以尝试写一个分类来自动打印出所有属性。
• 需求:能不能根据一个字典,自动生成对应的属性。
• 解决:提供一个分类,专门根据字典生成对应的属性字符串。
1 #import <Foundation/Foundation.h> 2 3 @interface NSDictionary (PropertyCode) 4 5 // 生成属性代码 6 - (void)createPropetyCode; 7 @end 8 9 @implementation NSDictionary (PropertyCode) 10 // 私有API:真实存在,但是苹果没有暴露出来,不给你用 11 // isKindOfClass:判断下是否是当前类或者子类 12 // 自动生成属性代码 13 - (void)createPropetyCode 14 { 15 NSMutableString *codes = [NSMutableString string]; 16 // 遍历字典 17 [self enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull value, BOOL * _Nonnull stop) { 18 NSString *code = nil; 19 20 if ([value isKindOfClass:[NSString class]]) {// 注:NSString *笔者喜欢用strong,若想使用copy可修改字符串 21 code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSString *%@;",key]; 22 } else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){ 23 code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",key]; 24 } else if ([value isKindOfClass:[NSNumber class]]) { 25 code = [NSString stringWithFormat:@"@property (nonatomic, assign) NSInteger %@;",key]; 26 } else if ([value isKindOfClass:[NSArray class]]) { 27 code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",key]; 28 } else if ([value isKindOfClass:[NSDictionary class]]) { 29 code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",key]; 30 } 31 32 // 拼接字符串 33 [codes appendFormat:@"\n%@\n",code]; 34 35 }]; 36 37 NSLog(@"%@",codes); 38 } 39 @end
外界使用:
1 #import "ViewController.h" 2 #import "Status.h" 3 #import "NSDictionary+PropertyCode.h" 4 /* 5 plist: 6 字典 7 字典转模型 8 */ 9 10 @interface ViewController () 11 12 @end 13 14 @implementation ViewController 15 16 - (void)viewDidLoad { 17 [super viewDidLoad]; 18 19 // 解析Plist 20 // 获取文件全路径 21 NSString *fileName = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil]; 22 // 获取字典 23 NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:fileName]; 24 25 // 设计模型-定义属性 26 // 自动生成属性代码 27 [dict createPropetyCode]; 28 } 29 @end
status.plist
控制台打印信息:
2016-04-15 10:43:34.959 Runtime(自动生成属性)[38837:1398902] @property (nonatomic, assign) BOOL aaa; @property (nonatomic, assign) NSInteger reposts_count; @property (nonatomic, copy) NSString *source; @property (nonatomic, strong) NSArray *pic_urls; @property (nonatomic, copy) NSString *created_at; @property (nonatomic, assign) NSInteger attitudes_count; @property (nonatomic, copy) NSString *idstr; @property (nonatomic, copy) NSString *text; @property (nonatomic, assign) NSInteger comments_count; @property (nonatomic, strong) NSDictionary *user;
二.字典转模型KVC实现
KVC必须要保证:模型中属性名要跟字典中key一一对应。
需求:开发中,通常后台会给你很多数据,但是并不是每个数据都有用,这些没有用的数据,需不需要保存到模型中?
使用KVC把字典转成模型:
#import <Foundation/Foundation.h> @interface Status : NSObject // 字典中有多少key,模型就有多少个属性 // 自动生成属性 -> 依赖字典 @property (nonatomic, assign) BOOL aaa; @property (nonatomic, strong) NSString *source; @property (nonatomic, assign) NSInteger reposts_count; @property (nonatomic, strong) NSArray *pic_urls; @property (nonatomic, strong) NSString *created_at; @property (nonatomic, assign) NSInteger attitudes_count; @property (nonatomic, strong) NSString *idstr; @property (nonatomic, strong) NSString *text; @property (nonatomic, assign) NSInteger comments_count; @property (nonatomic, strong) NSDictionary *user; // 定义属性 -> 设计模型 + (instancetype)statusWithDict:(NSDictionary *)dict; @end @implementation Status + (instancetype)statusWithDict:(NSDictionary *)dict{ // 创建模型 Status *s = [[self alloc] init]; // 字典value转模型属性保存 [s setValuesForKeysWithDictionary:dict]; return s; } @end
KVC的底层实现:
setValuesForKeysWithDictionary:遍历字典中所有key,去模型中查找对应的属性,把值给模型属性赋值。
//[s setValuesForKeysWithDictionary:dict];底层实现: [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { [s setValue:obj forKey:key]; }];
[s setValue:dict[@"source"] forKey:@"source"];
原理:
1.首先会去模型中查找有没有【setSource方法】,如果有 直接调用set方法 [s setSource:dict[@"source"]];
2.否则,去模型中查找有没有【source属性】,如果有 source = dict[@"source"]
3.否则,去模型中查找有没有【_source属性】,如果有 _source = dict[@"source"]
4.再否则,调用对象的 【setValue:forUndefinedKey:】直接报错
使用KVC最大的弊端:模型中属性名要跟字典中key一一对应,不对应就会报错:
2016-04-15 11:11:29.591 Runtime(字典转模型KVC实现)[39288:1411792] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<Status 0x7f9cb9f19680> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key source.'
那么,【重写setValue:forUndefinedKey:方法】,屏蔽系统报错就能解决模型中属性名要跟字典中key必须对应的问题。
@implementation Status + (instancetype)statusWithDict:(NSDictionary *)dict{ // 创建模型 Status *s = [[self alloc] init]; // 字典value转模型属性保存 [s setValuesForKeysWithDictionary:dict]; return s; } - (void)setValue:(id)value forUndefinedKey:(NSString *)key {} @end
这样就不用把字典中的所有key都写入模型了,只需要写我们想要的数据。
三.Runtime字典转模型一级转换
刚刚介绍了字典转模型的第一种方式---KVC,下面进入主题。
• 字典转模型的方式二:Runtime
◦ 思路:利用运行时,【遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值】。
◦ 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类来转。
1 #import <Foundation/Foundation.h> 2 3 @interface NSObject (Model) 4 5 + (instancetype)modelWithDict:(NSDictionary *)dict; 6 7 @end 8 9 10 #import <objc/message.h> 11 12 //class_copyPropertyList(__unsafe_unretained Class cls, unsigned int *outCount) 13 //这个方法只能获取【属性】列表(大括号里的成员变量不能获取) 14 15 @implementation NSObject (Model) 16 + (instancetype)modelWithDict:(NSDictionary *)dict 17 { 18 id objc = [[self alloc] init]; 19 20 // 获取成员变量列表 21 // 第一个参数class:获取哪个类成员变量列表 22 // 第二个参数count:成员变量总数 23 unsigned int count = 0; 24 25 // 成员变量数组 指向数组第0个元素 26 Ivar *ivarList = class_copyIvarList(self, &count); 27 28 // 遍历所有成员变量 29 for (int i = 0; i < count; i++) { 30 // 获取成员变量 31 Ivar ivar = ivarList[i]; 32 // 获取成员变量名称 33 NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; 34 35 // 成员变量名称转换key,去掉成员变量名称前面的下划线 36 NSString *key = [ivarName substringFromIndex:1]; 37 38 // 从字典中取出对应value 39 id value = dict[key]; 40 41 // 给模型中属性赋值 42 [objc setValue:value forKey:key]; 43 } 44 return objc; 45 } 46 @end
模型中只需要保留想要的属性名即可。
四.Runtime字典转模型二级转换
开发中可能会遇到,二级转换:如果字典中还有字典,也需要把对应的字典转换成模型,那么就需要二级转换
。
1 #import <Foundation/Foundation.h> 2 3 @interface NSObject (Model) 4 5 + (instancetype)modelWithDict:(NSDictionary *)dict; 6 7 @end 8 9 #import <objc/message.h> 10 11 @implementation NSObject (Model) 12 + (instancetype)modelWithDict:(NSDictionary *)dict 13 { 14 id objc = [[self alloc] init]; 15 16 unsigned int count = 0; 17 18 // 成员变量数组 指向数组第0个元素 19 Ivar *ivarList = class_copyIvarList(self, &count); 20 21 // 遍历所有成员变量 22 for (int i = 0; i < count; i++) { 23 24 // 获取成员变量 user 25 Ivar ivar = ivarList[i]; 26 // 获取成员变量名称 27 NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; 28 29 // 获取成员变量类型(后续判断是否进行二次转换) 30 NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; 31 NSLog(@"%@",type); 32 // @"@"User"" -> @"User" 33 type = [type stringByReplacingOccurrencesOfString:@"@\"" withString:@""]; 34 type = [type stringByReplacingOccurrencesOfString:@"\"" withString:@""]; 35 36 // 成员变量名称转换key 37 NSString *key = [ivarName substringFromIndex:1]; 38 39 // 从字典中取出对应value dict[@"user"] -> 字典 40 id value = dict[key]; 41 42 // 二级转换 43 // 并且是自定义类型,才需要转换 44 if ([value isKindOfClass:[NSDictionary class]] && ![type containsString:@"NS"]) { // 只有是字典,并且不包含NS才需要转换 45 46 //获取类名 47 Class className = NSClassFromString(type); 48 49 // 字典转模型 50 value = [className modelWithDict:value]; 51 } 52 53 // 给模型中属性赋值 key:user value:字典 ---> 模型 54 if (value) { 55 [objc setValue:value forKey:key]; 56 } 57 } 58 return objc; 59 } 60 @end