教你使用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
分类NSDictionary+PropertyCode

外界使用:

 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
ViewController

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
Status模型

KVC的底层实现:

setValuesForKeysWithDictionary:遍历字典中所有key,去模型中查找对应的属性,把值给模型属性赋值。

    //[s setValuesForKeysWithDictionary:dict];底层实现:

    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

        [s setValue:obj forKey:key];
    }];
setValuesForKeysWithDictionary:底层实现

 [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
NSObject+Model分类

模型中只需要保留想要的属性名即可。

四.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

posted on 2016-04-15 11:02  战斗宝宝007  阅读(395)  评论(0编辑  收藏  举报

导航