Mantle--國外程序員最經常使用的iOS模型&字典轉換框架

Mantle簡介

Mantle是iOS和Mac平臺下基於Objective-C編寫的一個簡單高效的模型層框架。html

Mantle能作什麼

Mantle能夠輕鬆把JSON數據、字典(Dictionary)和模型(即Objective對象)之間的相互轉換,支持自定義映射,而且內置實現了NSCoding和NSCoping,大大簡化歸檔操做。ios

爲何要使用Mantle

傳統的模型層方案遇到的問題

一般咱們用Objective-C寫的模型層遇到了什麼問題?git

咱們能夠用Github API來舉例。如今假設咱們想用Objective-C展示一個Github Issue,應該怎麼作?github

目前咱們能夠想到segmentfault

  1. 直接解析JSON數據字典,而後展示給UI數組

  2. 將JSON數據轉換爲模型,在賦值給UI服務器

關於1,弊端有不少,能夠參考個人這篇文章:在iOS開發中使用字典轉模型,如今假設咱們選擇了2,咱們大體會定義下面的GHIssue模型:網絡

GHIssue.happ

#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共同論道!

相關文章
相關標籤/搜索