字典轉模型架構 Mantle的使用:國外程式員最常用的iOS模型

來源:互聯網
上載者:User

標籤:

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 中的  fromJSONDictionary 和  JSONDictionaryFromModel 可以實現模型和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

 

字典轉模型架構 Mantle的使用:國外程式員最常用的iOS模型

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.