iOS开发·runtime+KVC实现多层字典模型转换(多层数据:模型嵌套模型,模型嵌套数组,数组嵌套模型)

本文实验Demo传送门:DictToModelDemo

前言:将后台JSON数据中的字典转成本地的模型,我们一般选用部分优秀的第三方框架,如SBJSON、JSONKit、MJExtension、YYModel等。但是,一些简单的数据,我们也可以尝试自己来实现转换的过程。

更重要的是,有时候在iOS面试的时候,部分面试官会不仅问你某种场景会用到什么框架,更会问你如果要你来实现这个功能,你有没有解决思路?所以,自己实现字典转模型还是有必要掌握的。有了这个基础,在利用运行时runtime的动态特性,你也可以实现这些第三方框架。

笔者的KVC系列为:

1. 字典转模型:KVC

当对象的属性很多的时候,我们可以利用KVC批量设置。

- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;

但是KVC批量转的时候,有个致命的缺点,就是当字典中的键,在对象属性中找不到对应的属性的时候会报错。解决办法是实现下面的方法:

//空的方法体也行
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}

需求:有一个排名列表页面,这个页面的每个排名对应一个模型,这个模型从Plist转换得到。那么实现的代码如下所示:

  • 头文件
#import <Foundation/Foundation.h>

@interface GloryListModel : NSObject

//图标
@property (nonatomic, copy) NSString *icon;
//标题
@property (nonatomic, copy) NSString *title;
//目标控制器
@property (nonatomic, copy) NSString *targetVC;
//菜单编号
@property (nonatomic, copy) NSString *menuCode;

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

+ (NSArray<GloryListModel *> *)gloryListModelsWithPlistName:(NSString *)plistName;

@end
  • 实现文件
#import "GloryListModel.h"

@implementation GloryListModel

//kvc实现字典转模型
- (instancetype)initWithDict:(NSDictionary *)dict{
    if (self = [super init]) {
        [self setValuesForKeysWithDictionary:dict];
    }
    return self;
}

//防止与后台字段不匹配而造成崩溃
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}

+ (instancetype)gloryListModelWithDict:(NSDictionary *)dict;{
    return [[self alloc]initWithDict:dict];
}

+ (NSArray<GloryListModel *> *)gloryListModelsWithPlistName:(NSString *)plistName;{
    //获取路径
    NSString *path = [[NSBundle mainBundle]pathForResource:plistName ofType:@"plist"];
    //读取plist
    NSArray *dictArr = [NSArray arrayWithContentsOfFile:path];
    //字典转模型
    NSMutableArray *modelArr = [NSMutableArray array];
    [dictArr enumerateObjectsUsingBlock:^(NSDictionary *dict, NSUInteger idx, BOOL * _Nonnull stop) {
        [modelArr addObject:[self gloryListModelWithDict:dict]];
    }];
    return modelArr.copy;
}

@end

1.2 KVC字典转模型弊端

弊端:必须保证,模型中的属性和字典中的key一一对应。
如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
报key找不到的错。

分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。

解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,
就能继续使用KVC,字典转模型了。

- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}

2. 字典转模型:Runtime

  • 思路1:利用运行时,首先要遍历参数字典, 如果我们获取得属性列表中包含了字典中的 key,就利用 KVC 方法赋值,然后就完成了字典转模型的操作。

  • 思路2:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值,然后就完成了字典转模型的操作。

至于实现途径,可以提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。

2.1 先遍历被转换的字典

  • 分类实现:NSObject+EnumDictOneLevel.m
#import "NSObject+EnumDictOneLevel.h"
#import <objc/runtime.h>

const char *kCMPropertyListKey1 = "CMPropertyListKey1";

@implementation NSObject (EnumDictOneLevel)

+ (instancetype)cm_modelWithDict1:(NSDictionary *)dict
{
    /* 实例化对象 */
    id model = [[self alloc]init];
    
    /* 使用字典,设置对象信息 */
    /* 1. 获得 self 的属性列表 */
    NSArray *propertyList = [self cm_objcProperties];
    
    /* 2. 遍历字典 */
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        
        /* 3. 判断 key 是否字 propertyList 中 */
        if ([propertyList containsObject:key]) {
            
            // KVC字典转模型
            if (obj) {
                /* 说明属性存在,可以使用 KVC 设置数值 */
                [model setValue:obj forKey:key];
            }
        }
        
    }];
    
    /* 返回对象 */
    return model;
}

+ (NSArray *)cm_objcProperties
{
    /* 获取关联对象 */
    NSArray *ptyList = objc_getAssociatedObject(self, kCMPropertyListKey1);
    
    /* 如果 ptyList 有值,直接返回 */
    if (ptyList) {
        return ptyList;
    }
    /* 调用运行时方法, 取得类的属性列表 */
    /* 成员变量:
     * class_copyIvarList(__unsafe_unretained Class cls, unsigned int *outCount)
     * 方法:
     * class_copyMethodList(__unsafe_unretained Class cls, unsigned int *outCount)
     * 属性:
     * class_copyPropertyList(__unsafe_unretained Class cls, unsigned int *outCount)
     * 协议:
     * class_copyProtocolList(__unsafe_unretained Class cls, unsigned int *outCount)
     */
    unsigned int outCount = 0;
    /**
     * 参数1: 要获取得类
     * 参数2: 雷属性的个数指针
     * 返回值: 所有属性的数组, C 语言中,数组的名字,就是指向第一个元素的地址
     */
    /* retain, creat, copy 需要release */
    objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
    
    NSMutableArray *mtArray = [NSMutableArray array];
    
    /* 遍历所有属性 */
    for (unsigned int i = 0; i < outCount; i++) {
        /* 从数组中取得属性 */
        objc_property_t property = propertyList[i];
        /* 从 property 中获得属性名称 */
        const char *propertyName_C = property_getName(property);
        /* 将 C 字符串转化成 OC 字符串 */
        NSString *propertyName_OC = [NSString stringWithCString:propertyName_C encoding:NSUTF8StringEncoding];
        [mtArray addObject:propertyName_OC];
    }
    
    /* 设置关联对象 */
    /**
     *  参数1 : 对象self
     *  参数2 : 动态添加属性的 key
     *  参数3 : 动态添加属性值
     *  参数4 : 对象的引用关系
     */
    
    objc_setAssociatedObject(self, kCMPropertyListKey1, mtArray.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    /* 释放 */
    free(propertyList);
    return mtArray.copy;
    
}

@end
  • 模型:PersonModel.h
#import <Foundation/Foundation.h>

@interface PersonModel : NSObject

@property (nonatomic, copy) NSString *iconStr;

@property (nonatomic, copy) NSString *showStr;

@end
  • 调用
NSDictionary *dict = @{
                           @"iconStr":@"小明",
                           @"showStr":@"这是我的第一条心情"
                           };
    PersonModel *testPerson = [PersonModel cm_modelWithDict1:dict];
    // 测试数据
    NSLog(@"%@",testPerson);
  • 运行验证

2.2 先遍历模型的成员变量数组

  • 实现分类:NSObject+EnumArr.m
#import "NSObject+EnumArr.h"
#import <objc/message.h>

@implementation NSObject (EnumArr)

/*
 * 把字典中所有value给模型中属性赋值,
 * KVC:遍历字典中所有key,去模型中查找
 * Runtime:根据模型中属性名去字典中查找对应value,如果找到就给模型的属性赋值.
 */
// 字典转模型
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 创建对应模型对象
    id objc = [[self alloc] init];
    
    
    unsigned int count = 0;
    
    // 1.获取成员属性数组
    Ivar *ivarList = class_copyIvarList(self, &count);
    
    // 2.遍历所有的成员属性名,一个一个去字典中取出对应的value给模型属性赋值
    for (int i = 0; i < count; i++) {
        
        // 2.1 获取成员属性
        Ivar ivar = ivarList[i];
        
        // 2.2 获取成员属性名 C -> OC 字符串
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];
        
        // 2.3 _成员属性名 => 字典key
        NSString *key = [ivarName substringFromIndex:1];
        
        // 2.4 去字典中取出对应value给模型属性赋值
        id value = dict[key];
        
        
        // 获取成员属性类型
        NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
        
        // 二级转换,字典中还有字典,也需要把对应字典转换成模型
        //
        // 判断下value,是不是字典
        if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { //  是字典对象,并且属性名对应类型是自定义类型
            // user User
            
            // 处理类型字符串 @\"User\" -> User
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
            ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
            // 自定义对象,并且值是字典
            // value:user字典 -> User模型
            // 获取模型(user)类对象
            Class modalClass = NSClassFromString(ivarType);
            
            // 字典转模型
            if (modalClass) {
                // 字典转模型 user
                value = [modalClass modelWithDict:value];
            }
            
            // 字典,user
            //            NSLog(@"%@",key);
        }
        
        // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
        // 判断值是否是数组
        if ([value isKindOfClass:[NSArray class]]) {
            // 判断对应类有没有实现字典数组转模型数组的协议
            if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                
                // 转换成id类型,就能调用任何对象的方法
                id idSelf = self;
                
                // 获取数组中字典对应的模型
                NSString *type =  [idSelf arrayContainModelClass][key];
                
                // 生成模型
                Class classModel = NSClassFromString(type);
                NSMutableArray *arrM = [NSMutableArray array];
                // 遍历字典数组,生成模型数组
                for (NSDictionary *dict in value) {
                    // 字典转模型
                    id model =  [classModel modelWithDict:dict];
                    [arrM addObject:model];
                }
                
                // 把模型数组赋值给value
                value = arrM;
                
            }
        }
        
        // 2.5 KVC字典转模型
        if (value) {
            
            [objc setValue:value forKey:key];
        }
    }
    
    
    // 返回对象
    return objc;
    
}

@end
  • 第1层模型:各个属性与字典对应
  • Status.h
#import <Foundation/Foundation.h>
#import "NSObject+EnumArr.h"

@class PersonModel;

@interface Status : NSObject <ModelDelegate>

@property (nonatomic, strong) NSString *title;
@property (nonatomic, strong) PersonModel *person;
@property (nonatomic, strong) NSArray *cellMdlArr;

@end
  • 第1层模型:实现文件需要指明数组里面装的类名
  • Status.m
#import "Status.h"

@implementation Status

+ (NSDictionary *)arrayContainModelClass
{
    return @{@"cellMdlArr" : @"CellModel"};
}

@end

  • 第2层模型:第2层模型作为第一层模型的自定义类的属性
  • PersonModel.h
#import <Foundation/Foundation.h>

@interface PersonModel : NSObject
@property (nonatomic, copy) NSString *iconStr;
@property (nonatomic, copy) NSString *showStr;

@end
  • 第2层模型:第2层模型作为第一层模型的数组类型的属性
  • CellModel.h
#import <Foundation/Foundation.h>

@interface CellModel : NSObject

@property (nonatomic, copy) NSString *stateStr;
@property (nonatomic, copy) NSString *partnerStr;

@end
  • 将被转换的字典
  • 运行验证

2.3 对2.1的改进:2.1无法对多层数据进行转换

思路:可以模仿2.2中的递归,对2.1进行改进:模型中,除了为数组属性添加数组元素对应的类名映射字典,还要为模型属性对应的类名添加映射字典。这是因为,从字典遍历出来的key无法得知自定义类型的属性的类名。

  • Status.m
+ (NSDictionary *)dictWithModelClass
{
    return @{@"person" : @"PersonModel"};
}
  • NSObject+EnumDict.m
#import "NSObject+EnumDict.h"

//导入模型
#import "Status.h"
#import <objc/runtime.h>

@implementation NSObject (EnumDict)

const char *kCMPropertyListKey = "CMPropertyListKey";

+ (instancetype)cm_modelWithDict:(NSDictionary *)dict
{
    /* 实例化对象 */
    id model = [[self alloc]init];
    
    /* 使用字典,设置对象信息 */
    /* 1. 获得 self 的属性列表 */
    NSArray *propertyList = [self cm_objcProperties];
    
    /* 2. 遍历字典 */
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        
        /* 3. 判断 key 是否字 propertyList 中 */
        if ([propertyList containsObject:key]) {
            
            // 获取成员属性类型
            // 类型经常变,抽出来
            NSString *ivarType;
            
            if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
                ivarType = @"NSString";
            }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
                ivarType = @"NSArray";
            }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
                ivarType = @"int";
            }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
                ivarType = @"NSDictionary";
            }
            
            // 二级转换,字典中还有字典,也需要把对应字典转换成模型
            // 判断下value,是不是字典
            if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]) { //  是字典对象,并且属性名对应类型是自定义类型
                // value:user字典 -> User模型
                // 获取模型(user)类对象
                NSString *ivarType = [Status dictWithModelClass][key];
                Class modalClass = NSClassFromString(ivarType);
                
                // 字典转模型
                if (modalClass) {
                    // 字典转模型 user
                    obj = [modalClass cm_modelWithDict:obj];
                }
                
            }
            
            // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
            // 判断值是否是数组
            if ([obj isKindOfClass:[NSArray class]]) {
                // 判断对应类有没有实现字典数组转模型数组的协议
                if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
                    
                    // 转换成id类型,就能调用任何对象的方法
                    id idSelf = self;
                    
                    // 获取数组中字典对应的模型
                    NSString *type =  [idSelf arrayContainModelClass][key];
                    
                    // 生成模型
                    Class classModel = NSClassFromString(type);
                    NSMutableArray *arrM = [NSMutableArray array];
                    // 遍历字典数组,生成模型数组
                    for (NSDictionary *dict in obj) {
                        // 字典转模型
                        id model =  [classModel cm_modelWithDict:dict];
                        [arrM addObject:model];
                    }
                    
                    // 把模型数组赋值给value
                    obj = arrM;
                    
                }
            }
            
            // KVC字典转模型
            if (obj) {
                /* 说明属性存在,可以使用 KVC 设置数值 */
                [model setValue:obj forKey:key];
            }
        }
        
    }];
    
    /* 返回对象 */
    return model;
}
  • 调用
// 解析Plist文件
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"statuses.plist" ofType:nil];
    NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath];

    // 获取字典数组
    NSArray *dictArr = statusDict[@"statuses"];
    NSMutableArray *statusArr = [NSMutableArray array];

    // 遍历字典数组
    for (NSDictionary *dict in dictArr) {

        Status *status = [Status cm_modelWithDict:dict];

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

推荐阅读更多精彩内容