iOS開發·runtime+KVC實現多層字典模型轉換(多層資料:模型巢狀模型,模型巢狀陣列,陣列巢狀模型)

陳滿iOS發表於2019-03-04

本文實驗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);
複製程式碼
  • 執行驗證
iOS開發·runtime+KVC實現多層字典模型轉換(多層資料:模型巢狀模型,模型巢狀陣列,陣列巢狀模型)

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
複製程式碼
  • 將被轉換的字典
iOS開發·runtime+KVC實現多層字典模型轉換(多層資料:模型巢狀模型,模型巢狀陣列,陣列巢狀模型)
  • 執行驗證
iOS開發·runtime+KVC實現多層字典模型轉換(多層資料:模型巢狀模型,模型巢狀陣列,陣列巢狀模型)

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);
複製程式碼
  • 執行驗證
iOS開發·runtime+KVC實現多層字典模型轉換(多層資料:模型巢狀模型,模型巢狀陣列,陣列巢狀模型)

相關文章