本文實(shí)驗(yàn)Demo傳送門:DictToModelDemo
前言:將后臺(tái)JSON數(shù)據(jù)中的字典轉(zhuǎn)成本地的模型,我們一般選用部分優(yōu)秀的第三方框架,如SBJSON、JSONKit、MJExtension、YYModel等。但是,一些簡(jiǎn)單的數(shù)據(jù),我們也可以嘗試自己來實(shí)現(xiàn)轉(zhuǎn)換的過程。
更重要的是,有時(shí)候在iOS面試的時(shí)候,部分面試官會(huì)不僅問你某種場(chǎng)景會(huì)用到什么框架,更會(huì)問你如果要你來實(shí)現(xiàn)這個(gè)功能,你有沒有解決思路?所以,自己實(shí)現(xiàn)字典轉(zhuǎn)模型還是有必要掌握的。有了這個(gè)基礎(chǔ),在利用運(yùn)行時(shí)runtime的動(dòng)態(tài)特性,你也可以實(shí)現(xiàn)這些第三方框架。
筆者的KVC系列為:
- iOS開發(fā)·KVC:字典轉(zhuǎn)模型,防止因本地未定義字段(后臺(tái)的字段與本地字符串名不一致)導(dǎo)致數(shù)據(jù)轉(zhuǎn)換過程中的奔潰
- iOS開發(fā)·runtime+KVC實(shí)現(xiàn)字典模型轉(zhuǎn)換
1. 字典轉(zhuǎn)模型:KVC
當(dāng)對(duì)象的屬性很多的時(shí)候,我們可以利用KVC批量設(shè)置。
- (void)setValuesForKeysWithDictionary:(NSDictionary<NSString *,id> *)keyedValues;
但是KVC批量轉(zhuǎn)的時(shí)候,有個(gè)致命的缺點(diǎn),就是當(dāng)字典中的鍵,在對(duì)象屬性中找不到對(duì)應(yīng)的屬性的時(shí)候會(huì)報(bào)錯(cuò)。解決辦法是實(shí)現(xiàn)下面的方法:
//空的方法體也行
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{}
需求:有一個(gè)排名列表頁面,這個(gè)頁面的每個(gè)排名對(duì)應(yīng)一個(gè)模型,這個(gè)模型從Plist轉(zhuǎn)換得到。那么實(shí)現(xiàn)的代碼如下所示:
- 頭文件
#import <Foundation/Foundation.h>
@interface GloryListModel : NSObject
//圖標(biāo)
@property (nonatomic, copy) NSString *icon;
//標(biāo)題
@property (nonatomic, copy) NSString *title;
//目標(biāo)控制器
@property (nonatomic, copy) NSString *targetVC;
//菜單編號(hào)
@property (nonatomic, copy) NSString *menuCode;
+ (instancetype)gloryListModelWithDict:(NSDictionary *)dict;
+ (NSArray<GloryListModel *> *)gloryListModelsWithPlistName:(NSString *)plistName;
@end
- 實(shí)現(xiàn)文件
#import "GloryListModel.h"
@implementation GloryListModel
//kvc實(shí)現(xiàn)字典轉(zhuǎn)模型
- (instancetype)initWithDict:(NSDictionary *)dict{
if (self = [super init]) {
[self setValuesForKeysWithDictionary:dict];
}
return self;
}
//防止與后臺(tái)字段不匹配而造成崩潰
- (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];
//字典轉(zhuǎn)模型
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字典轉(zhuǎn)模型弊端
弊端:必須保證,模型中的屬性和字典中的key一一對(duì)應(yīng)。
如果不一致,就會(huì)調(diào)用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
報(bào)key找不到的錯(cuò)。
分析:模型中的屬性和字典的key不一一對(duì)應(yīng),系統(tǒng)就會(huì)調(diào)用setValue:forUndefinedKey:
報(bào)錯(cuò)。
解決:重寫對(duì)象的setValue:forUndefinedKey:
,把系統(tǒng)的方法覆蓋,
就能繼續(xù)使用KVC,字典轉(zhuǎn)模型了。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
2. 字典轉(zhuǎn)模型:Runtime
思路1:利用運(yùn)行時(shí),首先要遍歷參數(shù)字典, 如果我們獲取得屬性列表中包含了字典中的 key,就利用 KVC 方法賦值,然后就完成了字典轉(zhuǎn)模型的操作。
思路2:利用運(yùn)行時(shí),遍歷模型中所有屬性,根據(jù)模型的屬性名,去字典中查找key,取出對(duì)應(yīng)的值,給模型的屬性賦值,然后就完成了字典轉(zhuǎn)模型的操作。
至于實(shí)現(xiàn)途徑,可以提供一個(gè)NSObject分類,專門字典轉(zhuǎn)模型,以后所有模型都可以通過這個(gè)分類轉(zhuǎn)。
2.1 先遍歷被轉(zhuǎn)換的字典
- 分類實(shí)現(xiàn):NSObject+EnumDictOneLevel.m
#import "NSObject+EnumDictOneLevel.h"
#import <objc/runtime.h>
const char *kCMPropertyListKey1 = "CMPropertyListKey1";
@implementation NSObject (EnumDictOneLevel)
+ (instancetype)cm_modelWithDict1:(NSDictionary *)dict
{
/* 實(shí)例化對(duì)象 */
id model = [[self alloc]init];
/* 使用字典,設(shè)置對(duì)象信息 */
/* 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字典轉(zhuǎn)模型
if (obj) {
/* 說明屬性存在,可以使用 KVC 設(shè)置數(shù)值 */
[model setValue:obj forKey:key];
}
}
}];
/* 返回對(duì)象 */
return model;
}
+ (NSArray *)cm_objcProperties
{
/* 獲取關(guān)聯(lián)對(duì)象 */
NSArray *ptyList = objc_getAssociatedObject(self, kCMPropertyListKey1);
/* 如果 ptyList 有值,直接返回 */
if (ptyList) {
return ptyList;
}
/* 調(diào)用運(yùn)行時(shí)方法, 取得類的屬性列表 */
/* 成員變量:
* 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)
* 協(xié)議:
* class_copyProtocolList(__unsafe_unretained Class cls, unsigned int *outCount)
*/
unsigned int outCount = 0;
/**
* 參數(shù)1: 要獲取得類
* 參數(shù)2: 雷屬性的個(gè)數(shù)指針
* 返回值: 所有屬性的數(shù)組, C 語言中,數(shù)組的名字,就是指向第一個(gè)元素的地址
*/
/* 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++) {
/* 從數(shù)組中取得屬性 */
objc_property_t property = propertyList[i];
/* 從 property 中獲得屬性名稱 */
const char *propertyName_C = property_getName(property);
/* 將 C 字符串轉(zhuǎn)化成 OC 字符串 */
NSString *propertyName_OC = [NSString stringWithCString:propertyName_C encoding:NSUTF8StringEncoding];
[mtArray addObject:propertyName_OC];
}
/* 設(shè)置關(guān)聯(lián)對(duì)象 */
/**
* 參數(shù)1 : 對(duì)象self
* 參數(shù)2 : 動(dòng)態(tài)添加屬性的 key
* 參數(shù)3 : 動(dòng)態(tài)添加屬性值
* 參數(shù)4 : 對(duì)象的引用關(guān)系
*/
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
- 調(diào)用
NSDictionary *dict = @{
@"iconStr":@"小明",
@"showStr":@"這是我的第一條心情"
};
PersonModel *testPerson = [PersonModel cm_modelWithDict1:dict];
// 測(cè)試數(shù)據(jù)
NSLog(@"%@",testPerson);
- 運(yùn)行驗(yàn)證
2.2 先遍歷模型的成員變量數(shù)組
- 實(shí)現(xiàn)分類:NSObject+EnumArr.m
#import "NSObject+EnumArr.h"
#import <objc/message.h>
@implementation NSObject (EnumArr)
/*
* 把字典中所有value給模型中屬性賦值,
* KVC:遍歷字典中所有key,去模型中查找
* Runtime:根據(jù)模型中屬性名去字典中查找對(duì)應(yīng)value,如果找到就給模型的屬性賦值.
*/
// 字典轉(zhuǎn)模型
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
// 創(chuàng)建對(duì)應(yīng)模型對(duì)象
id objc = [[self alloc] init];
unsigned int count = 0;
// 1.獲取成員屬性數(shù)組
Ivar *ivarList = class_copyIvarList(self, &count);
// 2.遍歷所有的成員屬性名,一個(gè)一個(gè)去字典中取出對(duì)應(yīng)的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 去字典中取出對(duì)應(yīng)value給模型屬性賦值
id value = dict[key];
// 獲取成員屬性類型
NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// 二級(jí)轉(zhuǎn)換,字典中還有字典,也需要把對(duì)應(yīng)字典轉(zhuǎn)換成模型
//
// 判斷下value,是不是字典
if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { // 是字典對(duì)象,并且屬性名對(duì)應(yīng)類型是自定義類型
// user User
// 處理類型字符串 @\"User\" -> User
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""];
ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
// 自定義對(duì)象,并且值是字典
// value:user字典 -> User模型
// 獲取模型(user)類對(duì)象
Class modalClass = NSClassFromString(ivarType);
// 字典轉(zhuǎn)模型
if (modalClass) {
// 字典轉(zhuǎn)模型 user
value = [modalClass modelWithDict:value];
}
// 字典,user
// NSLog(@"%@",key);
}
// 三級(jí)轉(zhuǎn)換:NSArray中也是字典,把數(shù)組中的字典轉(zhuǎn)換成模型.
// 判斷值是否是數(shù)組
if ([value isKindOfClass:[NSArray class]]) {
// 判斷對(duì)應(yīng)類有沒有實(shí)現(xiàn)字典數(shù)組轉(zhuǎn)模型數(shù)組的協(xié)議
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 轉(zhuǎn)換成id類型,就能調(diào)用任何對(duì)象的方法
id idSelf = self;
// 獲取數(shù)組中字典對(duì)應(yīng)的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍歷字典數(shù)組,生成模型數(shù)組
for (NSDictionary *dict in value) {
// 字典轉(zhuǎn)模型
id model = [classModel modelWithDict:dict];
[arrM addObject:model];
}
// 把模型數(shù)組賦值給value
value = arrM;
}
}
// 2.5 KVC字典轉(zhuǎn)模型
if (value) {
[objc setValue:value forKey:key];
}
}
// 返回對(duì)象
return objc;
}
@end
- 第1層模型:各個(gè)屬性與字典對(duì)應(yīng)
- 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層模型:實(shí)現(xiàn)文件需要指明數(shù)組里面裝的類名
- 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層模型作為第一層模型的數(shù)組類型的屬性
- CellModel.h
#import <Foundation/Foundation.h>
@interface CellModel : NSObject
@property (nonatomic, copy) NSString *stateStr;
@property (nonatomic, copy) NSString *partnerStr;
@end
- 將被轉(zhuǎn)換的字典
- 運(yùn)行驗(yàn)證
2.3 對(duì)2.1的改進(jìn):2.1無法對(duì)多層數(shù)據(jù)進(jìn)行轉(zhuǎn)換
思路:可以模仿2.2中的遞歸,對(duì)2.1進(jìn)行改進(jìn):模型中,除了為數(shù)組屬性添加數(shù)組元素對(duì)應(yīng)的類名映射字典,還要為模型屬性對(duì)應(yīng)的類名添加映射字典。這是因?yàn)椋瑥淖值浔闅v出來的key無法得知自定義類型的屬性的類名。
- Status.m
+ (NSDictionary *)dictWithModelClass
{
return @{@"person" : @"PersonModel"};
}
- NSObject+EnumDict.m
#import "NSObject+EnumDict.h"
//導(dǎo)入模型
#import "Status.h"
#import <objc/runtime.h>
@implementation NSObject (EnumDict)
const char *kCMPropertyListKey = "CMPropertyListKey";
+ (instancetype)cm_modelWithDict:(NSDictionary *)dict
{
/* 實(shí)例化對(duì)象 */
id model = [[self alloc]init];
/* 使用字典,設(shè)置對(duì)象信息 */
/* 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]) {
// 獲取成員屬性類型
// 類型經(jīng)常變,抽出來
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";
}
// 二級(jí)轉(zhuǎn)換,字典中還有字典,也需要把對(duì)應(yīng)字典轉(zhuǎn)換成模型
// 判斷下value,是不是字典
if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]) { // 是字典對(duì)象,并且屬性名對(duì)應(yīng)類型是自定義類型
// value:user字典 -> User模型
// 獲取模型(user)類對(duì)象
NSString *ivarType = [Status dictWithModelClass][key];
Class modalClass = NSClassFromString(ivarType);
// 字典轉(zhuǎn)模型
if (modalClass) {
// 字典轉(zhuǎn)模型 user
obj = [modalClass cm_modelWithDict:obj];
}
}
// 三級(jí)轉(zhuǎn)換:NSArray中也是字典,把數(shù)組中的字典轉(zhuǎn)換成模型.
// 判斷值是否是數(shù)組
if ([obj isKindOfClass:[NSArray class]]) {
// 判斷對(duì)應(yīng)類有沒有實(shí)現(xiàn)字典數(shù)組轉(zhuǎn)模型數(shù)組的協(xié)議
if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// 轉(zhuǎn)換成id類型,就能調(diào)用任何對(duì)象的方法
id idSelf = self;
// 獲取數(shù)組中字典對(duì)應(yīng)的模型
NSString *type = [idSelf arrayContainModelClass][key];
// 生成模型
Class classModel = NSClassFromString(type);
NSMutableArray *arrM = [NSMutableArray array];
// 遍歷字典數(shù)組,生成模型數(shù)組
for (NSDictionary *dict in obj) {
// 字典轉(zhuǎn)模型
id model = [classModel cm_modelWithDict:dict];
[arrM addObject:model];
}
// 把模型數(shù)組賦值給value
obj = arrM;
}
}
// KVC字典轉(zhuǎn)模型
if (obj) {
/* 說明屬性存在,可以使用 KVC 設(shè)置數(shù)值 */
[model setValue:obj forKey:key];
}
}
}];
/* 返回對(duì)象 */
return model;
}
- 調(diào)用
// 解析Plist文件
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"statuses.plist" ofType:nil];
NSDictionary *statusDict = [NSDictionary dictionaryWithContentsOfFile:filePath];
// 獲取字典數(shù)組
NSArray *dictArr = statusDict[@"statuses"];
NSMutableArray *statusArr = [NSMutableArray array];
// 遍歷字典數(shù)組
for (NSDictionary *dict in dictArr) {
Status *status = [Status cm_modelWithDict:dict];
[statusArr addObject:status];
}
NSLog(@"%@",statusArr);
- 運(yùn)行驗(yàn)證