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 如果你使用了类簇(关于类簇,请参考:类簇在iOS开发中的应用),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框架)

参考 MagicalRecord配合Mantle

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

http://yyny.me/ios/Mantle%E3%80%81JSONModel%E3%80%81MJExtension%E6%80%A7%E8%83%BD%E6%B5%8B%E8%AF%95/

PS: 本文由我们iOS122的小伙伴@TerwerGreen整理编辑,欢迎大家到他的个人博客terwer共同论道!

Mantle--国外程序员最常用的iOS模型&字典转换框架的更多相关文章

  1. 字典转模型框架 Mantle的使用:国外程序员最常用的iOS模型

    Mantle简介 Mantle 是iOS和Mac平台下基于Objective-C编写的一个简单高效的模型层框架. Mantle能做什么 Mantle可以轻松把JSON数据.字典(Dictionary) ...

  2. 国外程序员整理的Java资源大全分享

    Java 几乎是许多程序员们的入门语言,并且也是世界上非常流行的编程语言.国外程序员 Andreas Kull 在其 Github 上整理了非常优秀的 Java 开发资源,推荐给大家. 译文由 Imp ...

  3. 【转】国外程序员整理的Java资源大全

    Java几乎是许多程序员们的入门语言,并且也是世界上非常流行的编程语言.国外程序员Andreas Kull在其Github上整理了非常优秀的Java开发资源,推荐给大家.译文由ImportNew- 唐 ...

  4. 【转载】国外程序员整理的Java资源大全

    以下转载自: 推荐!国外程序员整理的Java资源大全中文版    https://github.com/akullpp/awesome-java英文版 Java 几乎是许多程序员们的入门语言,并且也是 ...

  5. [Mac A]为什么国外程序员爱用 Mac?

    from http://www.vpsee.com/2009/06/why-programmers-love-mac/ Mac 在国外很受欢迎,尤其是在 设计/web开发/IT 人员圈子里.普通用户喜 ...

  6. 推荐!国外程序员整理的 PHP 资源大全

    推荐!国外程序员整理的 PHP 资源大全 2014/08/02 · PHP, 工具与资源 · 8.5K 阅读 · 1 评论· php 分享到:0 与<YII框架>不得不说的故事—安全篇 R ...

  7. Java程序员最常用的8个Java日志框架

    转自:http://www.codeceo.com/article/8-java-log-framework.html 作为一名Java程序员,我们开发了很多Java应用程序,包括桌面应用.WEB应用 ...

  8. 为什么国外程序员爱用Mac?

    Mac 在国外很受欢迎,尤其是在 设计/web开发/IT 人员圈子里.普通用户喜欢 Mac 可以理解,毕竟 Mac 设计美观,简单好用,没有病毒.那么为什么专业人士也对 Mac 情有独钟呢?从个人使用 ...

  9. 转:Java程序员最常用的8个Java日志框架

    作为一名Java程序员,我们开发了很多Java应用程序,包括桌面应用.WEB应用以及移动应用.然而日志系统是一个成熟Java应用所必不可少的,在开发和调试阶段,日志可以帮助我们更好更快地定位bug:在 ...

随机推荐

  1. Kudu安装前的建议说明(博主推荐)

    不多说,直接上干货! 能点击进来看我写的这篇博文的朋友,肯定是刚入门的你. 其实以下是我从官网翻译过来的. http://kudu.apache.org/docs/installation.html# ...

  2. Linux 启动盘命令

    linux下有很多工具可以制作启动盘, 例如 unetbootin 和 wubi, 不过我们可以使用linux下的一条命令来完成-----dd 操作方法: 1 卸载你的U盘 假设你的u盘对应的设备是s ...

  3. Devexpress Xtrareport 创建主从报表

    效果 xtrareport 布局 From 代码 private DataSet Getdata() { DataSet ds = new DataSet(); //config配置字符串 strin ...

  4. js去除重复项

    window.onload = function(){ var array = [12, 14,15,17,12,11,12,14,16] alert(del(array)); } function ...

  5. 基于nodejs的DNS查询工具

    开始这个实例之前,我们简单谈一下Node.js吧,Node.js是一个由JavaScript书写而成的强大Web开发框架,它让开发强壮的.伸缩性良好的服务器端Web应用变得更加简单.容易.这种技术诞生 ...

  6. 微信小程序电商实战-商品详情(上)

    先看一下今天要实现的小程序商品详情页吧!   商品详情.gif 本期我们要实现小程序商品详情页的头部标题.头部轮播.商品详情浮动按钮和商品内页布局. 一.设置头部标题 如上图所示,头部标题是商品详情 ...

  7. Java对象转换成Json字符串是无法获得对应字段名

    问题: 代码中已经标注 @JSONField(name = "attrs") private String abc; public String getA() { return a ...

  8. Smile with face. Smile with mind.

    Smile with face. Smile with mind.微笑不仅是挂在脸上的,更是发自心底的.

  9. Android OS Startup

    OS puts emphases on how to provide interfaces to user's APPs for using hardware device in the conven ...

  10. spring-cloud构架微服务(2)-全局配置二

    接上篇,实际项目中,可能会遇到有些配置项,例如:邮件地址.手机号等在服务已经上线之后做了改动(就当会出现这种情况好了).然后你修改了配置信息,就得一个一个去重启对应的服务.spring-全局配置提供了 ...