现在大部分的项目都需要将服务器返回的JSON数据转换为Model再使用,手动转换不仅费时费力,还写了一堆重复代码,肯定是不科学的,一般都使用相应的工具来自动转换。目前接触的字典转模型工具有三种,Mantle, MJExtension, JSONModel, 虽然他们做的事情都是一样的,但是使用方法区别还是蛮大的,以及在一些细节上的处理也是不同的。

Mantle的使用

简单的例子就不来了,可以直接到Github上面查看,这里上一个比较典型全面的例子。



- (NSDictionary *)JSONDict {
    if (_JSONDict == nil) {
        _JSONDict = @{@"name" : [NSNull null],
                      @"age" : @20,
                      @"sex" : @0,
                      @"login_date" : @"1445136827",
                      @"phone" : @{
                              @"name" : @"小明的iPhone",
                              @"price" : @5000
                      }
                      @"books" : @[
                              @{@"name" : @"西游记"},
                          @{@"name" : @"三国演义"}
                      ]
                      };
    }
    return _JSONDict;
}


对应模型的特点: 1、有NSNull对象, 2、模型里面嵌套模型, 3、模型里面有数组,数组里面有模型.

对应的模型如下:



.h
typedef NS_ENUM(NSUInteger, Sex) {
    SexMale,
    SexFemale
};

@interface BookForMantle : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy, nullable) NSString *name;
@end

@interface PhoneForMantle : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy, nullable) NSString *name;
@property (nonatomic, assign) double price;
@end

@interface UserForMantle : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy, nullable) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign)  Sex sex;
@property (nonatomic, strong, nullable) NSDate *loginDate;
@property (nonatomic, strong, nullable) PhoneForMantle *phone;
@property (nonatomic, copy, nullable) NSArray<BookForMantle *> *books;
@end



.m
@implementation PhoneForMantle

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{@"name" : @"name",
             @"price" : @"price"};
}

@end

@implementation BookForMantle

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{@"name" : @"name"};
}

@end

@implementation UserForMantle

// 该map不光是JSON->Model, Model->JSON也会用到
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{@"name" : @"name",
             @"age" : @"age",
             @"sex" : @"sex",
             @"loginDate" : @"login_date",
             @"phone" : @"phone",
             @"books" : @"books"};
}

// 模型里面的模型
+ (NSValueTransformer *)phoneTransformer {
    return [MTLJSONAdapter dictionaryTransformerWithModelClass:[PhoneForMantle class]];
}

// 模型里面的数组
+ (NSValueTransformer *)booksTransformer {
    return [MTLJSONAdapter arrayTransformerWithModelClass:[BookForMantle class]];
}

// 时间
+ (NSValueTransformer *)loginDateJSONTransformer {
    return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *timeIntervalSince1970, BOOL *success, NSError *__autoreleasing *error) {
        NSTimeInterval timeInterval = [timeIntervalSince1970 doubleValue];
        NSDate *date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
        return date;
    } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
        NSTimeInterval timeInterval = date.timeIntervalSince1970;
        return @(timeInterval).stringValue;
    }];
}

@end


对应的解析方法:



// 注意: Mantle不会自动转类型,如:String->Int, 一旦类型不匹配,直接crash
// Json->Model
// 该方法会调用key-key map方法。
self.userForMantle = [MTLJSONAdapter modelOfClass:[UserForMantle class] fromJSONDictionary:self.JSONDict error:nil];
// 这种方式只是简单的使用KVC进行赋值。不会调用key-key map方法, 要求属性和JSON字典中的key名称相同,否则就crash
//    self.userForMantle = [UserForMantle modelWithDictionary:self.JSONDict error:&error];

// Model -> JSON
// 一旦有属性为nil, Mantle会转换成NSNull对象放到JSON字典中,这里有一个坑,使用NSUserDefault存储这样的JSON字典时,程序crash,原因是不可以包含NSNull对象。
NSDictionary *jsonDict = [MTLJSONAdapter JSONDictionaryFromModel:self.userForMantle error:nil];


JSOMModel的使用

仍旧使用上面的例子,对应的模型如下:



// BookForJsonModel
@protocol BookForJsonModel
@end

@interface BookForJsonModel : JSONModel
@property (nonatomic, copy, nullable) NSString *name;
@end

@implementation BookForJSONModel
// 前面是服务器字段,后面是模型属性字段
+ (JSONKeyMapper *)keyMapper {
    return [[JSONKeyMapper alloc] initWithDictionary:@{
       @"name" : @"name"
    }];
}
@end

// PhoneForJSONModel
@interface PhoneForJSONModel : JSONModel
@property (nonatomic, copy, nullable) NSString *name;
@property (nonatomic, assign) double price;
@end

@implementation PhoneForJSONModel
+ (JSONKeyMapper *)keyMapper {
    return [[JSONKeyMapper alloc] initWithDictionary:@{
       @"name" : @"name",
       @"price" : @"price"
    }];
}
@end

// UserForJSONModel
@interface UserForJSONModel : JSONModel
@property (nonatomic, copy, nullable) NSString *name;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign)  Sex sex;
@property (nonatomic, strong, nullable) NSDate *loginDate;
@property (nonatomic, strong, nullable) PhoneForJSONModel *phone;
// 注意协议
@property (nonatomic, copy, nullable) NSArray<BookForJSONModel> *books;
@end

@implementation UserForJSONModel
+ (JSONKeyMapper *)keyMapper {
    return [[JSONKeyMapper alloc] initWithDictionary:@{
       @"name" : @"name",
       @"age" : @"age",
       @"sex" : @"sex",
       @"login_date" : @"loginDate",
       @"phone" : @"phone",
       @"books" : @"books"
    }];
}

// 允许所有字段为空
+ (BOOL)propertyIsOptional:(NSString *)propertyName {
    return YES;
}
@end


对应的解析方法



// JSON->Model
UserForJSONModel *user = [[UserForJSONModel alloc] initWithDictionary:self.JSONDict error:nil];
// Model->JSON
NSDictionary *dict = [user toDictionary];


JSONModel各方面都挺好的,唯一需要注意的地方是它归档的方式, 它不是将对象归档,而是转换成字典再归档。



-(instancetype)initWithCoder:(NSCoder *)decoder
{
    NSString* json = [decoder decodeObjectForKey:@"json"];

    JSONModelError *error = nil;
    self = [self initWithString:json error:&error];
    if (error) {
        JMLog(@"%@",[error localizedDescription]);
    }
    return self;
}

-(void)encodeWithCoder:(NSCoder *)encoder
{
    [encoder encodeObject:self.toJSONString forKey:@"json"];
}

MJExtension的使用

这个的使用方式就不介绍了,GitHub上写的非常详细。这里主要说说我在用的项目中时遇到问题及解决方式。情况大致是这样的: 最开始项目中的模型统统继承自BaseModel类,解析方式都是自己挨个手动解析。还自定义了一些譬如时间戳转自定义日期类型的方法。在换到MJExtension时,没法对我们的自定义解析方式进行兼容,全部重写肯定是不现实的,只能做兼容。最后通过阅读MJExtension的源码,找到了一个突破口。在BaseModel里面对MJExtension里面的一个方法使用Method Swizzling进行替换。大致代码如下:



BaseModel.h 里面添加一个接口,子类可以覆盖
/// json->模型,转换完成之后调用, 以便进行自定义配置。
- (void)mc_keyValuesDidFinishConvertingToObjectWithData:(id)data;



BaseModel.m
/* 替换方法: - (instancetype)setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context error:(NSError **)error; */
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(setKeyValues:context:error:);
        SEL swizzledSelector = @selector(mc_setKeyValues:context:error:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (instancetype)mc_setKeyValues:(id)keyValues context:(NSManagedObjectContext *)context error:(NSError **)error {
    MCDataModel *model = [self mc_setKeyValues:keyValues context:context error:error];
    if ([self respondsToSelector:@selector(mc_keyValuesDidFinishConvertingToObjectWithData:)]) {
        [self mc_keyValuesDidFinishConvertingToObjectWithData:keyValues];
    }
    return model;
}


这样在使用MJExtension将模型解析完成之后再调用mc_keyValuesDidFinishConvertingToObjectWithData: 将原始数据传递过去进行自定义配置。就可以很好的与老工程兼容了。