Mantle--国外程序员最常用的iOS模型&字典转换框架

Mantle简介

Mantle是iOS和Mac平台下基于Objective-C编写的一个简单高效的模型层框架。

Mantle能做什么

Mantle可以轻松把JSON数据、字典(Dictionary)和模型(即Objective对象)之间的相互转换,支持自定义映射,并且内置实现了NSCoding和NSCoping,大大简化归档操作。

为什么要使用Mantle

传统的模型层方案遇到的问题

通常我们用Objective-C写的模型层遇到了什么问题?

我们可以用Github API来举例。现在假设我们想用Objective-C展现一个Github Issue,应该怎么做?

目前我们可以想到

  1. 直接解析JSON数据字典,然后展现给UI
  2. 将JSON数据转换为模型,在赋值给UI

关于1,弊端有很多,可以参考我的这篇文章:在iOS开发中使用字典转模型,现在假设我们选择了2,我们大致会定义下面的GHIssue模型:

GHIssue.h

    #import <Foundation/Foundation.h>

    typedef enum : NSUInteger {
        GHIssueStateOpen,
        GHIssueStateClosed
    } GHIssueState;
    
    @class GHUser;
    @interface GHIssue : NSObject <NSCoding, NSCopying>
    
    @property (nonatomic, copy, readonly) NSURL *URL;
    @property (nonatomic, copy, readonly) NSURL *HTMLURL;
    @property (nonatomic, copy, readonly) NSNumber *number;
    @property (nonatomic, assign, readonly) GHIssueState state;
    @property (nonatomic, copy, readonly) NSString *reporterLogin;
    @property (nonatomic, copy, readonly) NSDate *updatedAt;
    @property (nonatomic, strong, readonly) GHUser *assignee;
    @property (nonatomic, copy, readonly) NSDate *retrievedAt;
    
    @property (nonatomic, copy) NSString *title;
    @property (nonatomic, copy) NSString *body;
    
    - (instancetype)initWithDictionary:(NSDictionary *)dictionary;
    
    @end

GHIssue.m

    #import "GHIssue.h"
    #import "GHUser.h"
    
    @implementation GHIssue
    
    + (NSDateFormatter *)dateFormatter {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
        return dateFormatter;
    }
    
    - (instancetype)initWithDictionary:(NSDictionary *)dictionary {
        self = [self init];
        if (self == nil) return nil;
        
        _URL = [NSURL URLWithString:dictionary[@"url"]];
        _HTMLURL = [NSURL URLWithString:dictionary[@"html_url"]];
        _number = dictionary[@"number"];
        
        if ([dictionary[@"state"] isEqualToString:@"open"]) {
            _state = GHIssueStateOpen;
        } else if ([dictionary[@"state"] isEqualToString:@"closed"]) {
            _state = GHIssueStateClosed;
        }
        
        _title = [dictionary[@"title"] copy];
        _retrievedAt = [NSDate date];
        _body = [dictionary[@"body"] copy];
        _reporterLogin = [dictionary[@"user"][@"login"] copy];
        _assignee = [[GHUser alloc] initWithDictionary:dictionary[@"assignee"]];
        
        _updatedAt = [self.class.dateFormatter dateFromString:dictionary[@"updated_at"]];
        
        return self;
    }
    
    - (instancetype)initWithCoder:(NSCoder *)coder {
        self = [self init];
        if (self == nil) return nil;
        
        _URL = [coder decodeObjectForKey:@"URL"];
        _HTMLURL = [coder decodeObjectForKey:@"HTMLURL"];
        _number = [coder decodeObjectForKey:@"number"];
        _state = [coder decodeIntegerForKey:@"state"];
        _title = [coder decodeObjectForKey:@"title"];
        _retrievedAt = [NSDate date];
        _body = [coder decodeObjectForKey:@"body"];
        _reporterLogin = [coder decodeObjectForKey:@"reporterLogin"];
        _assignee = [coder decodeObjectForKey:@"assignee"];
        _updatedAt = [coder decodeObjectForKey:@"updatedAt"];
        
        return self;
    }
    
    - (void)encodeWithCoder:(NSCoder *)coder {
        if (self.URL != nil) [coder encodeObject:self.URL forKey:@"URL"];
        if (self.HTMLURL != nil) [coder encodeObject:self.HTMLURL forKey:@"HTMLURL"];
        if (self.number != nil) [coder encodeObject:self.number forKey:@"number"];
        if (self.title != nil) [coder encodeObject:self.title forKey:@"title"];
        if (self.body != nil) [coder encodeObject:self.body forKey:@"body"];
        if (self.reporterLogin != nil) [coder encodeObject:self.reporterLogin forKey:@"reporterLogin"];
        if (self.assignee != nil) [coder encodeObject:self.assignee forKey:@"assignee"];
        if (self.updatedAt != nil) [coder encodeObject:self.updatedAt forKey:@"updatedAt"];
        
        [coder encodeInteger:self.state forKey:@"state"];
    }
    
    - (instancetype)copyWithZone:(NSZone *)zone {
        GHIssue *issue = [[self.class allocWithZone:zone] init];
        issue->_URL = self.URL;
        issue->_HTMLURL = self.HTMLURL;
        issue->_number = self.number;
        issue->_state = self.state;
        issue->_reporterLogin = self.reporterLogin;
        issue->_assignee = self.assignee;
        issue->_updatedAt = self.updatedAt;
        
        issue.title = self.title;
        issue->_retrievedAt = [NSDate date];
        issue.body = self.body;
        
        return issue;
    }
    
    - (NSUInteger)hash {
        return self.number.hash;
    }
    
    - (BOOL)isEqual:(GHIssue *)issue {
        if (![issue isKindOfClass:GHIssue.class]) return NO;
        
        return [self.number isEqual:issue.number] && [self.title isEqual:issue.title] && [self.body isEqual:issue.body];
    }

GHUser.h

    @interface GHUser : NSObject <NSCoding, NSCopying>

    @property (nonatomic, copy) NSString *login;
    @property (nonatomic, assign) NSUInteger id;
    @property (nonatomic, copy) NSString *avatarUrl;
    @property (nonatomic, copy) NSString *gravatarId;
    @property (nonatomic, copy) NSString *url;
    @property (nonatomic, copy) NSString *htmlUrl;
    @property (nonatomic, copy) NSString *followersUrl;
    @property (nonatomic, copy) NSString *followingUrl;
    @property (nonatomic, copy) NSString *gistsUrl;
    @property (nonatomic, copy) NSString *starredUrl;
    @property (nonatomic, copy) NSString *subscriptionsUrl;
    @property (nonatomic, copy) NSString *organizationsUrl;
    @property (nonatomic, copy) NSString *reposUrl;
    @property (nonatomic, copy) NSString *eventsUrl;
    @property (nonatomic, copy) NSString *receivedEventsUrl;
    @property (nonatomic, copy) NSString *type;
    @property (nonatomic, assign) BOOL siteAdmin;
    
    - (id)initWithDictionary:(NSDictionary *)dictionary;
    
    @end

你会看到,如此简单的事情却有很多弊端。甚至,还有一些其他问题,这个例子里面没有展示出来。

  1. 无法使用服务器的新数据来更新这个 GHIssue
  2. 无法反过来将 GHIssue 转换成 JSON
  3. 对于GHIssueState,如果枚举改编了,现有的归档会崩溃
  4. 如果 GHIssue 接口改变了,现有的归档会崩溃。

使用MTLModel

如果使用MTLModel,我们可以这样,声明一个类继承自MTLModel

    typedef enum : NSUInteger {
        GHIssueStateOpen,
        GHIssueStateClosed
    } GHIssueState;
    
    @interface GHIssue : MTLModel <MTLJSONSerializing>
    
    @property (nonatomic, copy, readonly) NSURL *URL;
    @property (nonatomic, copy, readonly) NSURL *HTMLURL;
    @property (nonatomic, copy, readonly) NSNumber *number;
    @property (nonatomic, assign, readonly) GHIssueState state;
    @property (nonatomic, copy, readonly) NSString *reporterLogin;
    @property (nonatomic, strong, readonly) GHUser *assignee;
    @property (nonatomic, copy, readonly) NSDate *updatedAt;
    
    @property (nonatomic, copy) NSString *title;
    @property (nonatomic, copy) NSString *body;
    
    @property (nonatomic, copy, readonly) NSDate *retrievedAt;
    
    @end
    @implementation GHIssue
    
    + (NSDateFormatter *)dateFormatter {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
        return dateFormatter;
    }
    
    + (NSDictionary *)JSONKeyPathsByPropertyKey {
        return @{
            @"URL": @"url",
            @"HTMLURL": @"html_url",
            @"number": @"number",
            @"state": @"state",
            @"reporterLogin": @"user.login",
            @"assignee": @"assignee",
            @"updatedAt": @"updated_at"
        };
    }
    
    + (NSValueTransformer *)URLJSONTransformer {
        return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
    }
    
    + (NSValueTransformer *)HTMLURLJSONTransformer {
        return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
    }
    
    + (NSValueTransformer *)stateJSONTransformer {
        return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
            @"open": @(GHIssueStateOpen),
            @"closed": @(GHIssueStateClosed)
        }];
    }
    
    + (NSValueTransformer *)assigneeJSONTransformer {
        return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];
    }
    
    + (NSValueTransformer *)updatedAtJSONTransformer {
        return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
            return [self.dateFormatter dateFromString:dateString];
        } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
            return [self.dateFormatter stringFromDate:date];
        }];
    }
    
    - (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
        self = [super initWithDictionary:dictionaryValue error:error];
        if (self == nil) return nil;
    
        // Store a value that needs to be determined locally upon initialization.
        _retrievedAt = [NSDate date];
    
        return self;
    }
    
    @end

很明显,我们不需要再去实现<NSCoding>, <NSCopying>, -isEqual:-hash。在你的子类里面生命属性,MTLModel可以提供这些方法的默认实现。

最初例子里面的问题,在这里都得到了很好的解决。

  • MTLModel提供了一个- (void)mergeValueForKey:(NSString *)key fromModel:(id<MTLModel>)model{},可以与其他任何实现了MTLModel协议的模型对象集成。
  • +[MTLJSONAdapter JSONDictionaryFromModel:error:]可以把任何遵循MTLJSONSerializing>``协议的对象转换成JSON字典,+[MTLJSONAdapter JSONArrayFromModels:error:]```类似,不过转换的是一个数组。

MTLJSONAdapter中的fromJSONDictionaryJSONDictionaryFromModel可以实现模型和JSON的相互转化。

JSONKeyPathsByPropertyKey可以实现模型和JSON的自定义映射。

JSONTransformerForKey可以对JSON和模型不同类型进行映射。

classForParsingJSONDictionary 如果你使用了类簇,classForParsingJSONDictionary可以让你选择使用哪一个类进行JSON反序列化。

  • MTLModel可以用归档很好的存储模型而不需要去实现令人厌烦的NSCoding协议。 -decodeValueForKey:withCoder:modelVersion:方法在解码时会自动调用,如果重写,可以方便的进行自定义。

持久化

Mantle配合归档

MTLModel默认实现了 NSCoding协议,可以利用NSKeyedArchiver方便的对对象进行归档和解档。

Mantle配合Core Data

除了SQLite、FMDB之外,如果你想在你的数据里面执行复杂的查询,处理很多关系,支持撤销恢复,Core Data非常适合。

然而,这样也带来了一些痛点:

  • 仍然有很多弊端Managed objects解决了上面看到的一些弊端,但是Core Data自生也有他的弊端。正确的配置Core Data和获取数据需要很多行代码。
  • 很难保持正确性。甚至有经验的人在使用Core Data时也会犯错,并且这些问题框架是无法解决的。

如果你想获取JSON对象,Core Data需要做很多工作,但是却只能得到很少的回报。

但是,如果你已经在你的APP里面使用了Core Data,Mantle将仍然会是你的API和你的managed model objects之间一个很方便的转换层。

Mantle配合MagicRecord(一个Core Data框架)

Mantle为我们带来的好处

  • 实现了NSCopying protocol,子类可以直接copy是多么爽的事情
  • 实现了NSCoding protocol,跟NSUserDefaults说拜拜
  • 提供了-isEqual:和-hash的默认实现,model作NSDictionary的key方便了许多
  • 支持自定义映射,这在接口改变的情况下很有用
  • 简单且把一件事情做好,不掺杂网络相关的操作

合理选择

虽然上面说了一系列的好处,但如果你的App的代码规模只有几万行,或者API只有十几个,或者没有遇到上面这些问题, 建议还是不要引入了,杀鸡用指甲刀就够了。但是,Mantle的实现和思路是值得每位iOS工程师学习和借鉴的。

代码

https://github.com/terwer/MantleDemo

参考

https://github.com/mantle/mantle

http://segmentfault.com/a/1190000002431365

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏finleyMa

使用FlatList构建列表

接着上一篇 使用react-native-tab-navigator切换页面 当前首页页面内容是空的,只有一个背景色。下面我们来添加些内容。 这里使用 Fl...

763
来自专栏闻道于事

其他标签及框架集

今天学习了其他一些标签以及框架集。 <marquee> ... </marquee> 1 <marquee direction="down" heigh...

2807
来自专栏张俊红

网页的修饰

总第61篇 上一篇推送了网页的基本构成,链接地址:网页是怎么构成的?,这篇来讲讲网页的修饰,正如字面意思一般,本篇分享的内容是用来修饰网页的,是让网页变得更加好...

3587
来自专栏每日一篇技术文章

weex-07-通用布局

weex 常用的两种布局 分别为 flex弹性布局 和 绝对定位布局 一般情况下都是这两种配合使用!

611
来自专栏十月梦想

背景属性

623
来自专栏大前端开发

微信小程序新增拖动组件:movable-view

小程序在520节日前夜,让程序员们又躁动了一把,更新了一些很诱人的功能,如内容转发API,iBeacon API,振动API,屏幕亮度调节API等,也增强了地图...

764
来自专栏IMWeb前端团队

webcomponent学习笔记(一)

webcomponent是一个新的浏览器功能,为web提供了一个标准组件模型,包括以下几个部分: Shadow DOM Custom Elements HTML...

1950
来自专栏cnblogs

双飞翼布局的改造 box-sizing和margin负值的应用

box-sizing + margin负值 升级双飞翼布局 一、box-sizing属性 ? .content-size, .border-size{ ...

1898
来自专栏欧阳大哥的轮子

CSS中的float定位技术在iOS上的实现

几乎所有会WEB前端开发的同学都知道CSS中有一个float属性用于实现HTML元素的浮动定位展示。float 属性定义元素在哪个方向浮动。以往这个属性总应用于...

572
来自专栏技术小黑屋

纠结才能写出好代码

程序员的代码修炼应该有两个目标,一个是代码的执行效率,另一个是代码的可读性。朝着这两个目标努力的人很多,但是能够达到目标的人很少。

631

扫码关注云+社区