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 如果你使用了类簇(关于类簇,请参考:类簇在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共同论道!

转载于:https://www.cnblogs.com/ios122/p/4895208.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/458744.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++ assert() 详解

C assert 宏的应用方式将会在这篇文章中进行详解 相信对此有兴趣的朋友们应该可以根据我们介绍的内容充分掌握这方面的应用技巧。 作为一个经验丰富的编程人员来说&#xff0c;对于C编程语言应该不会陌生的&#xff0c;实现它的应用可以帮助我们轻松的各种功能需求。 在这里我…

直连测速服务器异常,求证! 网件R7800, Speedtest测速的怪现象,200M宽带+R7800者进...

本帖最后由 毛毛雨 于 2017-11-18 18:50 编辑宽带是联通FTTH 200M&#xff0c;标准千兆网线&#xff0c;千兆网卡。问题前的插曲&#xff1a;R7800刚到手&#xff0c;就迫不及待的换上了&#xff0c;结果&#xff0c;无论是路由器内置Speedtest册数&#xff0c;还是电脑端的Spe…

iOS socket

为什么80%的码农都做不了架构师&#xff1f;>>> #import "ViewController.h"interface ViewController ()<NSStreamDelegate,UITextFieldDelegate,UITableViewDataSource,UITableViewDelegate>{NSInputStream *_inputStream;//对应输入流NSOutputS…

PHP配置,php.ini以及覆盖问题

在部署一个cms项目到服务器上的时候&#xff0c;因为cms的模板比较老&#xff0c;服务器上用的php是5.3.3版&#xff08;大于5.3&#xff0c;可以认为是新的&#xff09;&#xff0c;有些页面会显示“deprecated”类别的错误信息。安全起见要抑制页面中的错误信息输出&#xff…

C/C++宏的使用总结

宏替换是C/C系列语言的技术特色&#xff0c;C/C语言提供了强大的宏替换功能&#xff0c;源代码在进入编译器之前&#xff0c;要先经过一个称为“预处理器”的模块&#xff0c;这个模块将宏根据编译参数和实际编码进行展开&#xff0c;展开后的代码才正式进入编译器&#xff0c;…

Macosx 安装 ionic 成功教程

2019独角兽企业重金招聘Python工程师标准>>> 一、首先介绍一下ionic ionic是一个用来开发混合手机应用的&#xff0c;开源的&#xff0c;免费的代码库。可以优化html、css和js的性能&#xff0c;构建高效的应用程序&#xff0c;而且还可以用于构建Sass和AngularJS的…

hp g6服务器安装系统,HPProLiantDL180G6服务器安装图.PDF

HPProLiantDL180G6服务器安装图4 前面板组件 / 25 个 2.5 英寸硬盘型号HP ProLiant DL180 G6 识别服务器组件2 光驱服务器 前面板组件 3 前部 UID LED 指示灯/开关4 系统运行状况 LED 指示灯1 前面板组件/4 个 3.5 英寸硬盘型号 5 网卡 1 活动 LED 指示灯安装图 6 网卡 2 活动 …

九度OJ 1076:N的阶乘 (数字特性、大数运算)

时间限制&#xff1a;3 秒 内存限制&#xff1a;128 兆 特殊判题&#xff1a;否 提交&#xff1a;6384 解决&#xff1a;2238 题目描述&#xff1a;输入一个正整数N&#xff0c;输出N的阶乘。 输入&#xff1a;正整数N(0<N<1000) 输出&#xff1a;输入可能包括多组数据&a…

Visual C++中 #include stdafx.h 头文件的用法

今天在做VC实验时&#xff0c;总是出现莫名其妙的错误。比如说&#xff1a; unexpected end of file whilelooking for precompiled header directive 再比如说这么一大串&#xff1a; mainframe.cpp 有错误\firstdlg.h(21) :error C2065: IDD_DIALOG_FIRST : undeclared ide…

mac显示无法连接adobe服务器,Mac安装Adobe软件,如遇Error提示解决方法

Mac10.15.3 安装Adobe Photoshop 2020的时候一直提示Error错误The installation cannot continue as the installer file may be damaged. Download the installer file again.看到这种问题&#xff0c;一般第一想法就是安装包损坏了&#xff0c;本能的会再下载一遍甚至多遍&am…

android开发中EditText自动获取焦点时隐藏hint的代码

只需让EditText设置以下的OnFocusChangeListener就可以了 private OnFocusChangeListener mOnFocusChangeListener new OnFocusChangeListener() {Overridepublic void onFocusChange(View v, boolean hasFocus){EditText textView (EditText)v;String hint;if (hasFocus) {h…

Grovvy初识

1.Groovy和Java对比 Groovy的松散的语法允许省略分号和修饰符除非另行指定&#xff0c;Grovvy的所有内容都为publicGrovvy允许定义简单脚本&#xff0c;同时无需定义正规的class对象Grovvy在普通的常用java对象上增加了一些独特的方法和快捷方式&#xff0c;使得他们更容易使用…

C和C++混合编程(__cplusplus使用)

第一种理解 比如说你用C开发了一个DLL库&#xff0c;为了能够让C语言也能够调用你的DLL输出(Export)的函数&#xff0c;你需要用extern "C"来强制编译器不要修改你的 函数名。 通常&#xff0c;在C语言的头文件中经常可以看到类似下面这种形式的代码&#xff1a; …

$.ajax 同步一不,ajax 同步不生效

可以用的生效代码注意 boolean 的位置var baseUrl ${pageContext.request.contextPath };function formcheck(){var flag false;var customerNameaa;var countryaa;var citybeijing;$.ajax({type: POST,url:baseUrl "/exports/credit/findBuyersBySerach",data:{&…

iOS工程中创建pch文件

1.新建pch类文件 2.在工程配置中,Build Setting 下搜索"pre"寻找Apple LLVM6.1 - Language下的 Preflx Header 3.点开Preflx Header 把左边pch类拖拽进去 4.把/"工程名"/....前边的内容全部换为$(SRCROOT) (具体替换内容看报错自己灵活运用)转载于:https:/…

批处理中setlocal enabledelayedexpansion的作用详细整理

设置本地为延迟扩展。其实也就是&#xff1a;延迟变量&#xff0c;全称延迟环境变量扩展, 想进阶&#xff0c;变量延迟是必过的一关&#xff01;所以这一部分希望你能认真看。 为了更好的说明问题&#xff0c;我们先引入一个例子。 例1: echo off set a4 set a5&echo…

一个服务器多个网站多个域名,多个域名一个服务器吗

多个域名一个服务器吗 内容精选换一换PAS(Primary Application Server)&#xff1a;主应用服务器。AAS(Additional Application Server)&#xff1a;扩展应用服务器。ASCS(ABAP Central Services)&#xff1a;SAP应用核心服务&#xff0c;是SAP应用的一个核心控件&#xff0c;包…

iframe 子父窗口互掉 js

一、父窗口调用iframe子窗口方法 1、HTML语法&#xff1a;<iframe name"myFrame" src"child.html"></iframe> 2、父窗口调用子窗口&#xff1a;myFrame.window.functionName(); 3、子窗品调用父窗口&#xff1a;parent.functionName(); 简单地…

yii2 ajax分页,Yii框架分页技术实例分析

本文实例讲述了Yii框架分页技术。分享给大家供大家参考&#xff0c;具体如下&#xff1a;直接上代码&#xff1a;1.首先写控制器层先引用pagination类use yii\data\Pagination;写自己的方法:function actionFenye(){$data Field::find(); //Field为model层,在控制器刚开始use了…

Spring源码解析——如何阅读源码

阅读目录 下面看一下如何使用jar包以及源码的source包  下面给出一个简单的spring样例  如何阅读源码最近没什么实质性的工作&#xff0c;正好有点时间&#xff0c;就想学学别人的代码。也看过一点源码&#xff0c;算是有了点阅读的经验&#xff0c;于是下定决心看下spring…