iOS 字典转模型 runtime实现

写在前面的话

这篇文章的通过runtime实现字典转模型是参考(抄袭)iOS 模式详解—「runtime面试、工作」看我就 🐒 了 _.中runtime 字典转模型,并且在此基础上做了以下扩展:

  1. 添加:属性名映射到字典中对应的key的方法,如id -> ID;
  2. 修复:当模型中的数组中不全是某一个模型的时候,会引起崩溃的问题。如数组中有8个元素,其中7个是模型,还有一个是字符串;

Github 传送门

需要考虑以下三种情况

  • 当字典中的key和模型的属性匹配不上;
  • 模型中嵌套模型(模型的属性是另外一个模型对象);
  • 模型中的数组中装着模型(数组中的元素是一个模型)。

一、使用runtime将字典转成模型

1. 思路

使用runtime遍历出模型中的所有属性,根据模型中属性,去字典中取出对应的value给模型属性赋值

2. 代码

1) 定义一个Student的模型,其属性如下:

Student.h文件

#import <Foundation/Foundation.h>

@interface Student : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, strong) NSNumber *height;
@property (nonatomic, strong) NSNumber *ID;


@end

Student.m文件

@implementation Student

@end

2)对NSObject扩展一个分类NSObject+DictionaryToModel

NSObject+DictionaryToModel.h文件

+ (instancetype)modelWithDict:(NSDictionary *)dict;

NSObject+DictionaryToModel.m文件,一定要导入<objc/message.h>

#import "NSObject+DictionaryToModel.h"
#import <objc/message.h>
@implementation NSObject (DictionaryToModel)
/*
 *  根据模型中属性,去字典中取出对应的value并赋值给模型的属性
 *  遍历取出所有属性
 */
+ (instancetype)modelWithDict:(NSDictionary *)dict {
    if (![dict isKindOfClass:[NSDictionary class]]) {
        return nil;
    }
    //1. 创建对应的对象
    id objc = [[self alloc] init];
    
    //2. 利用runtime给对象中的属性赋值
    /*
     Ivar: 成员变量;
     class_copyIvarList(): 获取类中的所有成员变量;
     第一个参数:表示获取哪个类的成员变量;
     第二个参数:表示这个类有多少成员变量;
     返回值Ivar *:指的是一个ivar数组,会把所有成员变量放在一个数组中,通过返回数组就全部获取到。
     count: 成员变量个数
     */
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];
        
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    
    return objc;
}

@end

3)调用+ (instancetype)modelWithDict:(NSDictionary *)dict


#import "Student.h"
#import "NSObject+DictionaryToModel.h"
......

NSDictionary *studentInfo = @{@"name" : @"Kobe Bryant",
                              @"age" : @(18),
                              @"height" : @(190),
                              @"id" : @(20160101),
                              @"gender" : @(1)};
Student *student = [Student modelWithDict2:studentInfo];
NSLog(@"student = %@", student);

4)模型的转换结果

image1.png

从上图可以看出

  • student.ID没有赋值成功,是因为在数据中没有ID这个key(Objective-C 中id是保留字,所以student的属性这里只能用ID)
  • 对于这种模型属性名和数据中key不对应的问题,接下来会讲如何解决。

<h3 id="2"></h3>

二、当字典中的key和模型的属性匹配不上

1. 思路

如果字典中的key和模型的属性匹配不上,可以做一个映射。将属性名映射到字典中对应的key上

2. 代码

这里代码接着上面的代码使用

1)在NSObject的分类NSObject+DictionaryToModel中添加映射方法

NSObject+DictionaryToModel.h文件中

+ (NSDictionary *)modelCustomPropertyMapper;

NSObject+DictionaryToModel.m文件中

#import "NSObject+DictionaryToModel.h"
#import <objc/message.h>
@implementation NSObject (DictionaryToModel)
/*
 *  根据模型中属性,去字典中取出对应的value并赋值给模型的属性
 *  遍历取出所有属性
 */
+ (instancetype)modelWithDict:(NSDictionary *)dict {
    if (![dict isKindOfClass:[NSDictionary class]]) {
        return nil;
    }
    //1. 创建对应的对象
    id objc = [[self alloc] init];
    
    //2. 利用runtime给对象中的属性赋值
    /*
     Ivar: 成员变量;
     class_copyIvarList(): 获取类中的所有成员变量;
     第一个参数:表示获取哪个类的成员变量;
     第二个参数:表示这个类有多少成员变量;
     返回值Ivar *:指的是一个ivar数组,会把所有成员变量放在一个数组中,通过返回数组就全部获取到。
     count: 成员变量个数
     */
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];
        
        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];
        
        //如果通过属性名取不到对应的value,则更换属性名对应的映射名来取值
        if (!value) {
            NSDictionary *customKeyDict = [self modelCustomPropertyMapper];
            NSString *customKey = customKeyDict[key];
            value = dict[customKey];
        }
        
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    
    return objc;
}

//这里放一个空的字典,真正实现这个映射方法的地方是在模型中,模型中会将此方法重写
+ (NSDictionary *)modelCustomPropertyMapper {
    return @{};
}

@end

2)在模型中实现映射方法

//重写NSObject+DictionaryToModel分类中的映射方法
+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"ID" : @"id"};
}

3)模型转换的结果如下

image2.png

<h3 id="3"></h3>

三、模型中嵌套模型

1. 思路

模型中嵌套模型就是字典中嵌套字典,当给模型的模型赋值的时候,再调用一次字典转模型就可以了。其实就是递归调用

2. 代码

1) 定义一个ZClass模型,其属性具体如下:

ZClass.h文件中,包含了Student模型。

#import <Foundation/Foundation.h>
#import "Student.h"

@interface ZClass : NSObject

@property (nonatomic, strong) Student *student;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;
@property (nonatomic, copy) NSString *header;

@end

ZClass.m文件中

#import "ZClass.h"
#import "NSObject+DictionaryToModel.h"

@implementation ZClass

@end

2) 在NSObject的分类NSObject+DictionaryToModel中

完善一下+ (instancetype)modelWithDict:(NSDictionary *)dict方法,这里我写在+ (instancetype)modelWithDict2:(NSDictionary *)dict中。

NSObject+DictionaryToModel.h文件

+ (instancetype)modelWithDict2:(NSDictionary *)dict;

NSObject+DictionaryToModel.m文件,一定要导入<objc/message.h>

+ (instancetype)modelWithDict2:(NSDictionary *)dict {
    if (![dict isKindOfClass:[NSDictionary class]]) {
        return nil;
    }
    id objc = [[self alloc] init];
    
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        // 获取成员变量类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        // 替换: @\"Student\" -> Student
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        NSString *key = [ivarName substringFromIndex:1];
        
        id value = dict[key];
        if (!value) {
            NSDictionary *customKeyDict = [self modelCustomPropertyMapper];
            NSString *customKey = customKeyDict[key];
            value = dict[customKey];
        }
        
        //如果value是一个字典,并且其类型是自定义对象才需要转换。不是OC中的数据类型,如:NSString, NSArray, NSDictionary, NSMutableArray, NSMutableDictionary, NSNumber等
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            Class modelClass = NSClassFromString(ivarType);
            
            if (modelClass) {
                   //如果modelClass存在,则进入递归调用
                value = [modelClass modelWithDict2:value];
            }
        }
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    
    return objc;
}

3)调用+ (instancetype)modelWithDict2:(NSDictionary *)dict

NSDictionary *classInfo = @{@"student" : studentInfo,
                                    @"title" : @"Math",
                                    @"subtitle" : @"Global",
                                    @"header" : @"Shanghai"};
        
ZClass *class1 = [ZClass modelWithDict2:classInfo];
NSLog(@"maxModel = %@",class1);

4)模型转换的结果如下

image3.png

<h3 id="4"></h3>

四、模型中的数组中装着模型

1. 思路

数组中装着模型,就是数组中的元素是一个字典。而这个字典对应着一个模型。在for循环数组的时候得到一个个字典,但是却不知道这个字典对应的模型是什么,所以需要告诉赋值的地方,数组中装的到底是什么模型,即模型的名称。

2. 代码

这里代码接着上面第三节的代码使用

1)在NSObject的分类NSObject+DictionaryToModel中添加 数组中包含模型名称的方法

在NSObject+DictionaryToModel.h文件中

+ (NSDictionary *)arrayContainModelClass;

在NSObject+DictionaryToModel.m文件中

+ (instancetype)modelWithDict2:(NSDictionary *)dict {
    if (![dict isKindOfClass:[NSDictionary class]]) {
        return nil;
    }
    id objc = [[self alloc] init];
    
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    for (int i = 0; i < count; i++) {
        Ivar ivar = ivarList[i];
        
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
        ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
        
        NSString *key = [ivarName substringFromIndex:1];
        
        id value = dict[key];
        if (!value) {
            NSDictionary *customKeyDict = [self modelCustomPropertyMapper];
            NSString *customKey = customKeyDict[key];
            value = dict[customKey];
        }
        
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) {
            Class modelClass = NSClassFromString(ivarType);
            
            if (modelClass) {
                value = [modelClass modelWithDict2:value];
            }
        }
        
        if ([value isKindOfClass:[NSArray class]]) {
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                // 转换成id类型,就能调用任何对象的方法
                id idSelf = self;
                // 获取数组中字典对应的模型
                NSString *type =  [idSelf arrayContainModelClass][key];
                if (type) {
                    // 生成模型
                    Class classModel = NSClassFromString(type);
                    NSMutableArray *arrM = [NSMutableArray array];
                    // 遍历字典数组,生成模型数组
                    for (NSDictionary *dict in value) {
                        // 字典转模型
                        id model =  [classModel modelWithDict2:dict];
                        if (model) {
                            [arrM addObject:model];
                        } else {
                            //如果数组中的某个元素并不是个字典,则不做解析
                            [arrM addObject:dict];
                        }
                    }
                    // 把模型数组赋值给value
                    value = arrM;
                }
            }
        }
        
        if (value) {
            [objc setValue:value forKey:key];
        }
    }
    
    return objc;
}

//这里放一个空的字典,真正实现这个方法的地方是在模型中,模型中会将此方法重写
+ (NSDictionary *)arrayContainModelClass {
    return @{};
}

2) 定义一个ZClass模型,其属性具体如下:

ZClass.h文件中,包含了Student模型。

#import <Foundation/Foundation.h>
#import "Student.h"

@interface ZClass : NSObject

@property (nonatomic, strong) Student *student;
@property (nonatomic, strong) NSArray *item;   //item中包含了Student类
@property (nonatomic, strong) NSDictionary *dict;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;
@property (nonatomic, copy) NSString *header;

@end

ZClass.m文件中

#import "ZClass.h"
#import "NSObject+DictionaryToModel.h"

@implementation ZClass

+ (NSDictionary *)arrayContainModelClass {
    return @{@"item" : @"Student"};
}

@end

3)调用+ (instancetype)modelWithDict2:(NSDictionary *)dict

NSDictionary *classInfo = @{@"student" : studentInfo,
                                    @"title" : @"Math",
                                    @"subtitle" : @"Global",
                                    @"header" : @"Shanghai",
                                    @"dict" : studentInfo,
                                    @"item" : @[studentInfo,studentInfo,@"whatever"]};
        
ZClass *class1 = [ZClass modelWithDict2:classInfo];
NSLog(@"maxModel = %@",class1);

4)模型转换的结果如下

image4.png

Github 传送门

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

推荐阅读更多精彩内容