iOS 開發(fā):Runtime(詳解六)字典轉(zhuǎn)模型

1较曼、簡介字典轉(zhuǎn)模型

在日常開發(fā)中磷斧,將網(wǎng)絡(luò)請求中獲取的 JSON 數(shù)據(jù)轉(zhuǎn)為數(shù)據(jù)模型,是我們開發(fā)中必不可少的操作捷犹。
通常我們會選用諸如 YYModel弛饭、JSONModel 或者 MJExtension 等第三方框架來實(shí)現(xiàn)這一過程。這些框架實(shí)現(xiàn)原理的核心就是 RuntimeKVC萍歉,以及 Getter / Setter侣颂。

實(shí)現(xiàn)的大體思路如下:借助 Runtime 可以動態(tài)獲取成員列表的特性,遍歷模型中所有屬性枪孩,然后以獲取到的屬性名為 key憔晒,在 JSON 字典中尋找對應(yīng)的值 value;再使用 KVC 或直接調(diào)用 Getter / Setter 將每一個(gè)對應(yīng) value賦值給模型蔑舞,就完成了字典轉(zhuǎn)模型的目的拒担。

需求:將服務(wù)器返回的 JSON 字典轉(zhuǎn)為數(shù)據(jù)模型。
先準(zhǔn)備一份待解析的 JSON 數(shù)據(jù)攻询,內(nèi)容如下:

{
    "id": "123412341234",
    "name": "心里只有你",
    "age": "18",
    "weight": 120,
    "address": {
        "country": "中國",
        "province": "北京"
    },
    "courses": [
        {
            "name": "Chinese",
            "desc": "語文課"
        },
        {
            "name": "Math",
            "desc": "數(shù)學(xué)課"
        },
        {
            "name": "English",
            "desc": "英語課"
        }
    ]
}

從這份 JSON 中可以看出从撼,字典中取值除了字符串之外,還有數(shù)組字典蜕窿。那么在將字典轉(zhuǎn)換成數(shù)據(jù)模型的時(shí)候谋逻,就要考慮 模型嵌套模型模型嵌套模型數(shù)組 的情況了桐经。

2毁兆、具體實(shí)現(xiàn)如下:

2.1、 創(chuàng)建模型

經(jīng)過分析阴挣,我們總共需要三個(gè)模型: XXStudentModel气堕、XXAdressModel、XXCourseModel。

/********************* XXStudentModel.h 文件 *********************/
#import <Foundation/Foundation.h>
#import "NSObject+XXModel.h"
@class XXAdressModel, XXCourseModel;

@interface XXStudentModel : NSObject <XXModel>

/* 姓名 */
@property (nonatomic, copy) NSString *name;
/* 學(xué)生號 id */
@property (nonatomic, copy) NSString *uid;
/* 年齡 */
@property (nonatomic, assign) NSInteger age;
/* 體重 */
@property (nonatomic, assign) NSInteger weight;
/* 地址(嵌套模型) */
@property (nonatomic, strong) XXAdressModel *address;
/* 課程(嵌套模型數(shù)組) */
@property (nonatomic, strong) NSArray *courses;

@end

/********************* XXStudentModel.m 文件 *********************/
#import "XXStudentModel.h"
#import "XXCourseModel.h"

@implementation XXStudentModel

+ (NSDictionary *)modelContainerPropertyGenericClass {
    //需要特別處理的屬性
    return @{
             @"courses" : [XXCourseModel class],
             @"uid" : @"id"
             };
}

@end


/********************* XXAdressModel.h 文件 *********************/
#import <Foundation/Foundation.h>

@interface XXAdressModel : NSObject

/* 國籍 */
@property (nonatomic, copy) NSString *country;
/* 省份 */
@property (nonatomic, copy) NSString *province;
/* 城市 */
@property (nonatomic, copy) NSString *city;

@end


/********************* XXAdressModel.m 文件 *********************/
#import "XXAdressModel.h"

@implementation XXAdressModel

@end


/********************* XXCourseModel.h 文件 *********************/
#import <Foundation/Foundation.h>

@interface XXCourseModel : NSObject

/* 課程名 */
@property (nonatomic, copy) NSString *name;
/* 課程介紹 */
@property (nonatomic, copy) NSString *desc;

@end

/********************* XXCourseModel.m 文件 *********************/
#import "XXCourseModel.h"

@implementation XXCourseModel

@end

2.2茎芭、 在 NSObject 分類中實(shí)現(xiàn)字典轉(zhuǎn)模型

NSObject+XXModel.h揖膜、NSObject+XXModel.m 就是我們用來解決字典轉(zhuǎn)模型所創(chuàng)建的分類,協(xié)議中的 + (NSDictionary *)modelContainerPropertyGenericClass 方法用來告訴分類特殊字段的處理規(guī)則梅桩,比如 id --> uid壹粟。

/********************* NSObject+XXModel.h 文件 *********************/
#import <Foundation/Foundation.h>

// XXModel 協(xié)議
@protocol XXModel <NSObject>

@optional
// 協(xié)議方法:返回一個(gè)字典,表明特殊字段的處理規(guī)則
+ (nullable NSDictionary<NSString *, id> *)modelContainerPropertyGenericClass;

@end;

@interface NSObject (XXModel)

// 字典轉(zhuǎn)模型方法
+ (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary;

@end
/********************* NSObject+XXModel.m 文件 *********************/
#import "NSObject+XXModel.h"
#import <objc/runtime.h>

@implementation NSObject (XXModel)

+ (instancetype)xx_modelWithDictionary:(NSDictionary *)dictionary {
    
    // 創(chuàng)建當(dāng)前模型對象
    id object = [[self alloc] init];
    
    unsigned int count;
    
    // 獲取當(dāng)前對象的屬性列表
    objc_property_t *propertyList = class_copyPropertyList([self class], &count);
    
    // 遍歷 propertyList 中所有屬性宿百,以其屬性名為 key趁仙,在字典中查找 value
    for (unsigned int i = 0; i < count; i++) {
        // 獲取屬性
        objc_property_t property = propertyList[i];
        const char *propertyName = property_getName(property);
        
        NSString *propertyNameStr = [NSString stringWithUTF8String:propertyName];
        
        // 獲取 JSON 中屬性值 value
        id value = [dictionary objectForKey:propertyNameStr];
        
        // 獲取屬性所屬類名
        NSString *propertyType;
        unsigned int attrCount;
        objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount);
        for (unsigned int i = 0; i < attrCount; i++) {
            switch (attrs[i].name[0]) {
                case 'T': { // Type encoding
                    if (attrs[i].value) {
                        propertyType = [NSString stringWithUTF8String:attrs[i].value];
                        // 去除轉(zhuǎn)義字符:@\"NSString\" -> @"NSString"
                        propertyType = [propertyType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
                        // 去除 @ 符號
                        propertyType = [propertyType stringByReplacingOccurrencesOfString:@"@" withString:@""];
                        
                    }
                } break;
                default: break;
            }
        }
        
        // 對特殊屬性進(jìn)行處理
        // 判斷當(dāng)前類是否實(shí)現(xiàn)了協(xié)議方法,獲取協(xié)議方法中規(guī)定的特殊屬性的處理方式
        NSDictionary *perpertyTypeDic;
        if([self respondsToSelector:@selector(modelContainerPropertyGenericClass)]){
            perpertyTypeDic = [self performSelector:@selector(modelContainerPropertyGenericClass) withObject:nil];
        }
        
        // 處理:字典的 key 與模型屬性不匹配的問題垦页,如 id -> uid
        id anotherName = perpertyTypeDic[propertyNameStr];
        if(anotherName && [anotherName isKindOfClass:[NSString class]]){
            value =  dictionary[anotherName];
        }

        // 處理:模型嵌套模型的情況
        if ([value isKindOfClass:[NSDictionary class]] && ![propertyType hasPrefix:@"NS"]) {
            Class modelClass = NSClassFromString(propertyType);
            if (modelClass != nil) {
                // 將被嵌套字典數(shù)據(jù)也轉(zhuǎn)化成Model
                value = [modelClass xx_modelWithDictionary:value];
            }
        }

        // 處理:模型嵌套模型數(shù)組的情況
        // 判斷當(dāng)前 value 是一個(gè)數(shù)組雀费,而且存在協(xié)議方法返回了 perpertyTypeDic
        if ([value isKindOfClass:[NSArray class]] && perpertyTypeDic) {
            Class itemModelClass = perpertyTypeDic[propertyNameStr];
            //封裝數(shù)組:將每一個(gè)子數(shù)據(jù)轉(zhuǎn)化為 Model
            NSMutableArray *itemArray = @[].mutableCopy;
            for (NSDictionary *itemDic  in value) {
                id model = [itemModelClass xx_modelWithDictionary:itemDic];
                [itemArray addObject:model];
            }
            value = itemArray;
        }

        // 使用 KVC 方法將 value 更新到 object 中
        if (value != nil) {
            [object setValue:value forKey:propertyNameStr];
        }
        
    }
    free(propertyList);
    
    return object;
}

@end

2.3、 測試代碼

- (void)parseJSON {
    
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"Student" ofType:@"json"];
    NSData *jsonData = [NSData dataWithContentsOfFile:filePath];

    // 讀取 JSON 數(shù)據(jù)
    NSDictionary *json = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
    NSLog(@"%@",json);

    // JSON 字典轉(zhuǎn)模型
    XXStudentModel *student = [XXStudentModel xx_modelWithDictionary:json];

    NSLog(@"student.uid = %@", student.uid);
    NSLog(@"student.name = %@", student.name);

    for (unsigned int i = 0; i < student.courses.count; i++) {
        XXCourseModel *courseModel = student.courses[i];
        NSLog(@"courseModel[%d].name = %@ .desc = %@", i, courseModel.name, courseModel.desc);
    }
}

當(dāng)然痊焊,如若需要考慮緩存機(jī)制盏袄、性能問題、對象類型檢查等薄啥,建議還是使用例如 YYModel 之類的知名第三方框架辕羽,或者自己造輪子。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末罪佳,一起剝皮案震驚了整個(gè)濱河市逛漫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌赘艳,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件克握,死亡現(xiàn)場離奇詭異蕾管,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)菩暗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進(jìn)店門掰曾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人停团,你說我怎么就攤上這事旷坦。” “怎么了佑稠?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵秒梅,是天一觀的道長。 經(jīng)常有香客問我舌胶,道長捆蜀,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮辆它,結(jié)果婚禮上誊薄,老公的妹妹穿的比我還像新娘。我一直安慰自己锰茉,他們只是感情好呢蔫,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著飒筑,像睡著了一般片吊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扬霜,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天定鸟,我揣著相機(jī)與錄音,去河邊找鬼著瓶。 笑死联予,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的材原。 我是一名探鬼主播沸久,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼余蟹!你這毒婦竟也來了卷胯?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤威酒,失蹤者是張志新(化名)和其女友劉穎窑睁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體葵孤,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡担钮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尤仍。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片箫津。...
    茶點(diǎn)故事閱讀 40,127評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖宰啦,靈堂內(nèi)的尸體忽然破棺而出苏遥,到底是詐尸還是另有隱情,我是刑警寧澤赡模,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布田炭,位于F島的核電站,受9級特大地震影響纺裁,放射性物質(zhì)發(fā)生泄漏诫肠。R本人自食惡果不足惜司澎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望栋豫。 院中可真熱鬧挤安,春花似錦、人聲如沸丧鸯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丛肢。三九已至围肥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蜂怎,已是汗流浹背穆刻。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杠步,地道東北人氢伟。 一個(gè)月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像幽歼,于是被迫代替她去往敵國和親朵锣。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容