Mantle 初探,修改使其支持任意嵌套 Array

昨晚,学习了一下 Mantle,参照的是这篇 blog,首先感谢原文作者,基本熟悉了 Mantle 的使用方法,但那篇文章比较久远了,Mantle 现在对类型转换加入了 ErrorHandling 机制,如果你的 model 声明类型和解析 Json 得到的不一致,就要你自己提供 xxxJSONTransformer 方法,如果找不到该方法,就会绑定失败,这点要特别注意。

更新:去翻 Mantle 的 issue 列表,发现 value 是数组这种情况可以通过 属性名+JSONTransformer 方法对数组对象进行转换,作者认为这种转换不是框架该做的事情,不过每个属性都要用 属性名+JSONTransformer 确实也很麻烦,谁叫咱懒呢,文末给出这种解决方案

原文还有一个坑要注意的是大家注意解析到的 json"Weather"对应的 value 是一个 array

因此在 + (NSDictionary *)JSONKeyPathsByPropertyKey 方法中这样写是绑定不成功的,会导致整个 Model 为 nil

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
                ...
             @"conditionDescription": @"weather.description",
                ...

            };
}

K神 的讨论是 这种情况比较麻烦一些,需要创建一个新的 model,然后将他反序列化出来 。但我们还是想用 weather.description,毕竟这种最方便么。于是我打断点跑了一下 Mantle 的源码,发现主要是 "NSDictionary+MTLJSONKeyPath.h/NSDictionary+MTLJSONKeyPath.m" 这个类在控制着解析过程,我们来具体看一下:

#import "NSDictionary+MTLJSONKeyPath.h"

#import "MTLJSONAdapter.h"

@implementation NSDictionary (MTLJSONKeyPath)

- (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error {

    //1. 传进来的 JSONKeyPath 其实就是 "weather.description",然后把他们每一部分都放进数组中
    NSArray *components = [JSONKeyPath componentsSeparatedByString:@"."];

    //2. 遍历 componets 数组
    id result = self;
    for (NSString *component in components) {
        // Check the result before resolving the key path component to not
        // affect the last value of the path.
        if (result == nil || result == NSNull.null) break;

        //3. 检查 result,不是字典对象就返回错误,并最终返回 nil 
        if (![result isKindOfClass:NSDictionary.class]) {
            if (error != NULL) {
                NSDictionary *userInfo = @{
                    NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid JSON dictionary", @""),
                    NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"JSON key path %1$@ could not resolved because an incompatible JSON dictionary was supplied: \"%2$@\"", @""), JSONKeyPath, self]
                };

                *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
            }

            if (success != NULL) *success = NO;

            return nil;
        }
        // 4. 取出相应 key 所对应的值
        result = result[component];
    }

    // 5. 成功并返回
    if (success != NULL) *success = YES;

    return result;
}

@end

上面就是解析 "aaa.bbb" 这种语法的写法,我们观察一下注释 3 就能发现该写法只支持 NSDictionary 对象,也就是说,只有 "aaa.bbb.ccc.ddd.eee" 中的 "aaa", "bbb", "ccc", "ddd", "eee" 都为 NSDictionary 对象时,才能解析成功,如果其中任意一个为数组对象就会解析失败,例如我们上面的 weather.description,知道原因了,就好办多了,让我们来修改一下。

首先增加一个 NSArrayCategory NSArray+NTLJSONKeyPath,内容和 NSDictionary+MTLJSONKeyPath 差不多,来看下实现:

#import "NSArray+NTLJSONKeyPath.h"
#import "MTLJSONAdapter.h"

@implementation NSArray (NTLJSONKeyPath)
- (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error {

    NSUInteger count = self.count;

    id result;
    for (int i = 0; i < count; i++) {
        // Check the result before resolving the key path component to not
        // affect the last value of the path.
        result = self[i];
        if (result == nil || result == NSNull.null) break;

        if (![result isKindOfClass:NSDictionary.class] && ![result isKindOfClass:[NSArray class]]) {
            if (error != NULL) {
                NSDictionary *userInfo = @{
                                           NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid JSON dictionary", @""),
                                           NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"JSON key path %1$@ could not resolved because an incompatible JSON dictionary was supplied: \"%2$@\"", @""), JSONKeyPath, self]
                                           };

                *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
            }

            if (success != NULL) *success = NO;

            return nil;
        }
        if ([result isKindOfClass:NSDictionary.class]) {
            result = result[JSONKeyPath];
            if (result) break;
        } else if ([result isKindOfClass:[NSArray class]]) {
            result = [(NSArray *)result mtl_valueForJSONKeyPath:JSONKeyPath success:success error:error];
        }

    }

    if (success != NULL) *success = YES;

    return result;

}

代码也非常简单,如果遇到数组了,就遍历,遍历到当前数组元素是字典就根据当前 key,找出对应的 value,如果 value 还是数组,就继续递归执行。接着把 NSDictionary+MTLJSONKeyPath 小修改一下:

#import "NSDictionary+MTLJSONKeyPath.h"
#import "NSArray+NTLJSONKeyPath.h"

#import "MTLJSONAdapter.h"

@implementation NSDictionary (MTLJSONKeyPath)

- (id)mtl_valueForJSONKeyPath:(NSString *)JSONKeyPath success:(BOOL *)success error:(NSError **)error {
    NSArray *components = [JSONKeyPath componentsSeparatedByString:@"."];

    id result = self;
    for (NSString *component in components) {
        // Check the result before resolving the key path component to not
        // affect the last value of the path.
        if (result == nil || result == NSNull.null) break;

        // 1. 这里增加了对 result 是否为数组的判断,如果是数组,就不要返回 nil 了
        if (![result isKindOfClass:NSDictionary.class] && ![result isKindOfClass:[NSArray class]]) {
            if (error != NULL) {
                NSDictionary *userInfo = @{
                    NSLocalizedDescriptionKey: NSLocalizedString(@"Invalid JSON dictionary", @""),
                    NSLocalizedFailureReasonErrorKey: [NSString stringWithFormat:NSLocalizedString(@"JSON key path %1$@ could not resolved because an incompatible JSON dictionary was supplied: \"%2$@\"", @""), JSONKeyPath, self]
                };

                *error = [NSError errorWithDomain:MTLJSONAdapterErrorDomain code:MTLJSONAdapterErrorInvalidJSONDictionary userInfo:userInfo];
            }

            if (success != NULL) *success = NO;

            return nil;
        }

        //2. 判断 result 的类型,执行相应的方法
        if ([result isKindOfClass:NSDictionary.class]) {
            result = result[component];
        } else if ([result isKindOfClass:[NSArray class]]) {
            result = [(NSArray *)result mtl_valueForJSONKeyPath:component success:success error:error];
        }

    }

    if (success != NULL) *success = YES;

    return result;
}

只用修改上面两处,Binggo,一个可以处理 weather.description 的 Mantle 就改好啦,什么?weather.description 太简单,那我们试一下面的 'weather.pp.aa.dd'

解析结果:

是不是还不错呢,本篇的 demo 和 json 我都上传到 github 上了,可以自行试验,也许会有 bug,不过有什么问题可以给我留言


另一种解决方案 issue 源地址

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
             ...
             @"conditionDescription": @"weather",
             @"condition": @"weather",
             @"icon": @"weather",
             ...
             };
}

+ (NSValueTransformer *)conditionDescriptionJSONTransformer {
    return [MTLValueTransformer transformerUsingForwardBlock:^id(NSArray *weather, BOOL *success, NSError **error) {
        return [weather.firstObject valueForKey:@"description"];
    }];
}

-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!