- GitHub: Mantle
- Star: 11.2k
Mantle——iOS 模型 & 字典轉換框架
Mantle 是 iOS 和 Mac 平臺下基于 Objective-C 編寫的一個簡單高效的模型層框架戒傻。
典型的模型對象
通常情況下瓦戚,用 Objective-C 編寫模型對象的方式存在哪些問題?
讓我們用 GitHub API 進行演示纬凤。在 Objective-C 中捐寥,如何用一個模型來表示 GitHub
issue 笤昨?
typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
@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;
- (id)initWithDictionary:(NSDictionary *)dictionary;
@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;
}
- (id)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;
}
- (id)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 decodeUnsignedIntegerForKey:@"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 encodeUnsignedInteger:self.state forKey:@"state"];
}
- (id)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];
}
@end
哇,這么簡單的事情就編寫了很多樣板代碼握恳!而且瞒窒,即使如此,此示例仍無法解決一些問題:
- 無法使用服務器的新數(shù)據(jù)更新
GHIssue
對象乡洼。 - 無法反過來將
GHIssue
對象轉換回 JSON 模型崇裁。 -
GHIssueState
不應原樣編碼。如果這個枚舉類型將來發(fā)生了變更束昵,則現(xiàn)有的歸檔會崩潰(無法向下兼容)拔稳。 - 如果
GHIssue
的接口未來發(fā)生變化,則現(xiàn)有的歸檔會崩潰(無法向下兼容)锹雏。
為什么不使用 Core Data?
Core Data 很好地解決了某些問題巴比。如果你需要對數(shù)據(jù)執(zhí)行復雜的查詢,處理具有大量關系的巨大對象圖或支持撤消和重做礁遵,那么 Core Data 是一個很好的選擇轻绞。
但是,它確實也有一些痛點:
- 仍然需要編寫很多樣板代碼佣耐。管理對象減少了上面看到的一些樣板代碼政勃,但是 Core Data 有很多自己的東西。正確設置 Core Data 堆棧(持久性存儲和持久性存儲協(xié)調器)并執(zhí)行提取操作可能也需要編寫不少代碼兼砖。
- 它很難正確工作奸远。即使是經驗豐富的開發(fā)人員既棺,在使用 Core Data 時也會犯錯,并且該框架也讓人難以忍受懒叛。
如果你只是想嘗試訪問 JSON 對象援制,Core Data 可能需要耗費很多功夫而收效甚微(投入大于收益,不劃算)芍瑞。
盡管如此,如果你已經在應用程序中使用或想要使用 Core Data褐墅,Mantle 仍然可以是 API 和模型對象之間的便捷轉換層拆檬。
MTLModel
使用 MTLModel。這是繼承自 MTLModel
對象的 GHIssue
對象示例:
typedef enum : NSUInteger {
GHIssueStateOpen,
GHIssueStateClosed
} GHIssueState;
// !!!: 必須遵守 <MTLJSONSerializing> 協(xié)議
@interface GHIssue : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy, readonly) NSURL *URL; // URL 類型
@property (nonatomic, copy, readonly) NSURL *HTMLURL; // URL 類型
@property (nonatomic, copy, readonly) NSNumber *number;
@property (nonatomic, assign, readonly) GHIssueState state; // 枚舉類型
@property (nonatomic, copy, readonly) NSString *reporterLogin;
@property (nonatomic, strong, readonly) GHUser *assignee; // 該屬性指向 GHUser 對象實例
@property (nonatomic, copy, readonly) NSDate *updatedAt; // JSON 日期字符串妥凳,轉換為 NSDate
@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;
}
// 模型和 JSON 的自定義映射
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"URL" : @"url",
@"HTMLURL" : @"html_url",
@"number" : @"number",
@"state" : @"state",
@"reporterLogin" : @"user.login",
@"assignee" : @"assignee",
@"updatedAt" : @"updated_at"
};
}
// 自定義 JSON 模型轉換竟贯,URL -> NSURL
+ (NSValueTransformer *)URLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
// 自定義 JSON 模型轉換,URL -> NSURL
+ (NSValueTransformer *)HTMLURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
// 自定義 JSON 模型轉換逝钥,JSON 字符串 -> 枚舉類型
+ (NSValueTransformer *)stateJSONTransformer {
return [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
@"open": @(GHIssueStateOpen),
@"closed": @(GHIssueStateClosed)
}];
}
// assignee 屬性是一個 GHUser 對象實例
+ (NSValueTransformer *)assigneeJSONTransformer {
return [MTLJSONAdapter dictionaryTransformerWithModelClass:GHUser.class];
}
// 自定義 JSON 模型轉換屑那,JSON 字符串 -> NSDate
+ (NSValueTransformer *)updatedAtJSONTransformer {
return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) {
// 自定義 JSON 轉模型方式
return [self.dateFormatter dateFromString:dateString];
} reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) {
// 自定義模型轉 JSON 方式
return [self.dateFormatter stringFromDate:date];
}];
}
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
self = [super initWithDictionary:dictionaryValue error:error];
if (self == nil) return nil;
// 存儲需要在初始化時由本地確定的值
_retrievedAt = [NSDate date];
return self;
}
@end
此版本中明顯沒有 <NSCoding>
,
<NSCopying>
艘款,-isEqual:
持际,和 -hash
的方法實現(xiàn)。通過檢查子類中的 @property
屬性聲明哗咆,MTLModel
可以為所有這些方法提供默認實現(xiàn)蜘欲。
原始示例中的問題也都被修復了:
無法使用服務器中的新數(shù)據(jù)更新
GHIssue
對象。
MTLModel
擴展了一個的 -mergeValuesForKeys: FromModel:
方法晌柬,可以與其他任何實現(xiàn)了<MTLModel>
協(xié)議的模型對象集成姥份。
無法將
GHIssue
模型轉換回 JSON 對象。
這就是反向轉換器真正派上用場的地方年碘。
+[MTLJSONAdapter JSONDictionaryFromModel:error:]
可以把任何遵守 <MTLJSONSerializing>
協(xié)議的模型對象轉換回 JSON 字典澈歉。
+[MTLJSONAdapter JSONArrayFromModels:error:]
是同樣的,但是它是將包含模型對象的數(shù)組轉換為 JSON 數(shù)組屿衅。
如果
GHIssue
的接口發(fā)生變化埃难,則現(xiàn)有存檔可能會無法工作。
MTLModel
會自動保存用于歸檔的模型對象的版本傲诵。當解檔時凯砍,如果覆寫了 -decodeValueForKey:withCoder:modelVersion:
方法,它會被自動調用拴竹,從而為你提供方便的掛鉤(hook)來升級舊數(shù)據(jù)悟衩。
MTLJSONSerializing - 模型和 JSON 的相互轉換
為了將模型對象從 JSON 序列化或序列化為 JSON,你需要在自定義的 MTLModel
子類對象中聲明該子類對象遵守<MTLJSONSerializing>
協(xié)議栓拜。這樣就可以使用 MTLJSONAdapter
將模型對象從 JSON 轉換回來:
// JSON -> Model
NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class fromJSONDictionary:JSONDictionary error:&error];
// Model -> JSON
NSError *error = nil;
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:user error:&error];
+JSONKeyPathsByPropertyKey - 實現(xiàn)模型和 JSON 的自定義映射
此方法返回的 NSDictionary
字典用于指定如何將模型對象的屬性映射到 JSON 的鍵上座泳。
@interface XYUser : MTLModel <MTLJSONSerializing>
@property (readonly, nonatomic, copy) NSString *name;
@property (readonly, nonatomic, strong) NSDate *createdAt;
@property (readonly, nonatomic, assign, getter = isMeUser) BOOL meUser;
@property (readonly, nonatomic, strong) XYHelper *helper;
@end
@implementation XYUser
// 模型和 JSON 的自定義映射
// 將模型對象的屬性名稱與 JSON 對象的 key 名稱進行映射惠昔。
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"name": @"name",
@"createdAt": @"created_at"
};
}
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error {
self = [super initWithDictionary:dictionaryValue error:error];
if (self == nil) return nil;
_helper = [XYHelper helperWithName:self.name createdAt:self.createdAt];
return self;
}
@end
在此示例中,XYUser
類聲明了 Mantle
需要以不同方式處理的四個屬性:
-
name
屬性被映射到了 JSON 中相同名稱的鍵上挑势。 -
createdAt
屬性映射到了其等效的 snack 語法格式的鍵上镇防。 -
meUser
屬性沒有序列化為 JSON。 - JSON 反序列化后潮饱,
helper
屬性會在本地被初始化来氧。
如果模型的父類還遵守了 <MTLJSONSerializing>
協(xié)議,則使用 -[NSDictionary mtl_dictionaryByAddingEntriesFromDictionary:]
來合并其映射香拉。
如果你想將模型類的所有屬性映射到它們自己啦扬,則可以使用+[NSDictionary mtl_identityPropertyMapWithModel:]
輔助方法。
使用 +[MTLJSONAdapter modelOfClass:fromJSONDictionary:error:]
方法反序列化 JSON 時凫碌,與屬性名稱不對應或具有顯式映射的 JSON 將被忽略:
NSDictionary *JSONDictionary = @{
@"name": @"john",
@"created_at": @"2013/07/02 16:40:00 +0000",
@"plan": @"lite"
};
NSError *error = nil;
XYUser *user = [MTLJSONAdapter modelOfClass:XYUser.class
fromJSONDictionary:JSONDictionary
error:&error];
/**
<XYUser: 0x280d99170> {
helper = <XYHelper: 0x2803c99e0> {
name = john,
createdAt = 2013-07-02 16:40:00 +0000
}
*/
該示例中扑毡, plan
字段將會被忽略,因為它既不匹配 XYUser
的屬性名稱盛险,也不映射到+JSONKeyPathsByPropertyKey
中瞄摊。
+JSONTransformerForKey:
- 對 JSON 和模型不同類型手動進行映射
從 JSON 反序列化時,實現(xiàn)這個 <MTLJSONSerializing>
協(xié)議中可選的方法以將屬性轉換為其他類型苦掘。
??
將 JSON 對象轉換為模型對象時换帜,如果 JSON 對象的數(shù)據(jù)類型和模型對象的數(shù)據(jù)類型不一致,或者無法實現(xiàn)自動轉換時鸟蜡,需要通過以下的方法進行手動轉換膜赃。
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
此方法支持批量的自定義映射!通過判斷屬性名
key
的不同揉忘,可以實現(xiàn)多個屬性的自定義映射操作跳座。
// 注意:該方法中的局部參數(shù) key 指的是「模型對象」中的屬性名稱。
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key {
if ([key isEqualToString:@"createdAt"]) {
// 當處理 createdAt 屬性的映射時泣矛,執(zhí)行自定義轉換
return [NSValueTransformer valueTransformerForName:XYDateValueTransformerName];
}
return nil;
}
key
是應用于模型對象的屬性名疲眷;不是原始的 JSON 中的鍵。如果你使用 +JSONKeyPathsByPropertyKey
轉換時您朽,請記住這一點狂丝。
為了更加方便,如果你實現(xiàn)了 +<key>JSONTransformer
方法哗总,那么 MTLJSONAdapter
將改用該方法的結果几颜。例如,JSON 中通常表示為字符串的日期可以轉換為 NSDate
讯屈,如下所示:
// 自定義 JSON 模型轉換蛋哭,JSON 字符串 -> NSDate
+ (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];
}];
}
如果轉換器是可逆的,則在將對象序列化為 JSON 時也將使用它涮母。
??
也就是說谆趾,屬性的自定義轉換支付兩種方法躁愿,一種是:
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
它支持批量的自定義映射操作。
還有一種是單個屬性的自定義映射方法沪蓬,即:
+<key>JSONTransformer;
這邊的
<key>
是模型對象中屬性的名字彤钟。以上面的GHIssue
例子來說,GHIssue
對象中的第一個屬性URL
是NSURL
類型的屬性跷叉,而 JSON 模型返回的 URL 鏈接是一個字符串類型逸雹,它們之間的數(shù)據(jù)類型不一致,因此這個屬性無法實現(xiàn)自動轉換云挟,需要手動實現(xiàn)峡眶,即:// 自定義 JSON 模型轉換,URL -> NSURL // 這個方法中的 <key> 就是 URL植锉,即模型中的 URL 屬性。 + (NSValueTransformer *)URLJSONTransformer { return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName]; }
也就是說每個單獨實現(xiàn)的自定義轉換方法名是通過模型屬性名與
JSONTransformer
拼接而來的峭拘。另外俊庇,這個 “拼接形式” 的自定義模型轉換方法的優(yōu)先級比
JSONTransformerForKey:
要高!也就是說鸡挠,如果兩個方法中都實現(xiàn)了某一個屬性的自定義 JSON 模型轉換辉饱,則以+<key>JSONTransformer;
方法的實現(xiàn)為準!
+classForParsingJSONDictionary:
如果你使用了類簇拣展,請實現(xiàn)此可選方法彭沼,classForParsingJSONDictionary
可以讓你選擇使用哪一個類進行 JSON 反序列化。
@interface XYMessage : MTLModel
@end
@interface XYTextMessage: XYMessage
@property (readonly, nonatomic, copy) NSString *body;
@end
@interface XYPictureMessage : XYMessage
@property (readonly, nonatomic, strong) NSURL *imageURL;
@end
@implementation XYMessage
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary {
if (JSONDictionary[@"image_url"] != nil) {
return XYPictureMessage.class;
}
if (JSONDictionary[@"body"] != nil) {
return XYTextMessage.class;
}
NSAssert(NO, @"No matching class for the JSON dictionary '%@'.", JSONDictionary);
return self;
}
@end
然后备埃,MTLJSONAdapter
會根據(jù)你傳入的 JSON 字典自動選擇類:
NSDictionary *textMessage = @{
@"id": @1,
@"body": @"Hello World!"
};
NSDictionary *pictureMessage = @{
@"id": @2,
@"image_url": @"http://example.com/lolcat.gif"
};
XYTextMessage *messageA = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:textMessage error:NULL];
XYPictureMessage *messageB = [MTLJSONAdapter modelOfClass:XYMessage.class fromJSONDictionary:pictureMessage error:NULL];
Persistence 持久化存儲
Mantle 不會自動為你保留對象姓惑。但是,MTLModel 默認實現(xiàn)了 NSCoding
協(xié)議按脚,可以利用 NSKeyedArchiver
方便的對對象進行歸檔和解檔于毙。
如果你需要更強大的功能,或者想要避免一次將整個模型保留在內存中辅搬,那么 Core Data 可能是更好的選擇。
最低系統(tǒng)要求
Mantle supports the following platform deployment targets:
- macOS 10.10+
- iOS 8.0+
- tvOS 9.0+
- watchOS 2.0+
導入 Mantle
手動導入
To add Mantle to your application:
- Add the Mantle repository as a submodule of your application's repository.
- Run
git submodule update --init --recursive
from within the Mantle folder. - Drag and drop
Mantle.xcodeproj
into your application's Xcode project. - On the "General" tab of your application target, add
Mantle.framework
to the "Embedded Binaries".
If you’re instead developing Mantle on its own, use the Mantle.xcworkspace
file.
Carthage 方式
Simply add Mantle to your Cartfile
:
github "Mantle/Mantle"
CocoaPods 方式
Add Mantle to your Podfile
under the build target they want it used in:
target 'MyAppOrFramework' do
pod 'Mantle'
end
Then run a pod install
within Terminal or the CocoaPods app.
License
Mantle is released under the MIT license. See
LICENSE.md.
More Info
Have a question? Please open an issue!