本文翻譯自Simplifying RESTful API Use and Data Persistence on iOS with Mantle and Realm
iOS開發(fā)者都會熟悉Core Data,蘋果的一種圖形化對象的存儲框架.除了本地存儲外,它還有許多高級特性,比如對象的追蹤和撤銷.這些特性在某些情形非常有用,但學(xué)習(xí)起來并不那么容易,需要寫大量的代碼,學(xué)習(xí)曲線很陡.
2014年,Realm作為專為移動開發(fā)所設(shè)計的數(shù)據(jù)庫,一經(jīng)發(fā)布就引起開發(fā)界的注意.如果僅作為本地存儲,Realm是很好的選擇.畢竟并不是所有人都會用到Core Data的高級特性.Realm相當(dāng)好用,且與CoreData不同的是它僅需要極少的代碼.而且它是線程安全的,據(jù)說要快于蘋果的存儲框架.
在大多數(shù)的app中,數(shù)據(jù)存儲占據(jù)重要的位置.我們經(jīng)常要從遠(yuǎn)程服務(wù)器中通過RESTful API來解析數(shù)據(jù),此時需要用到Mantle,它是用于Cocoa和Cocoa Touch模型轉(zhuǎn)換的開源庫.Mantle可以很輕松地完成JSON接口的模型轉(zhuǎn)換工作.

本文我們將用紐約時報的搜索API來創(chuàng)建一個文章列表的應(yīng)用.用的是標(biāo)準(zhǔn)的HTTP GET請求,接收和請求的模型用的是Mantle.我們將看到用Mantle來處理相應(yīng)的值轉(zhuǎn)換(例如NSDate到string)是多么方便.接收數(shù)據(jù)后,我們用Realm來存儲它們.很少的代碼即可完成以上操作.
- 開始RESTful API
我們先來創(chuàng)建一個”Master-Detail Application”的iOS項(xiàng)目,名字為“RealmMantleTurorial”.用CocoaPods來管理依賴庫.Podfile文件需要以下內(nèi)容:
pod 'Mantle'
pod 'Realm'
pod 'AFNetworking'
pods安裝完成后,打開新創(chuàng)建的MantleRealmTutorialworkspace.你會發(fā)現(xiàn),著名的網(wǎng)絡(luò)庫AFNetworking也已經(jīng)安裝好了.我們將用它來做API請求.
正如前文所說,紐約時報為我們提供了相當(dāng)好用的文章搜索API.我們需要申請一個access key來使用它.去這里 完成此操作.當(dāng)我們申請完API key時,就可以開始編碼工作了.
在創(chuàng)建Mantle數(shù)據(jù)模型之前,需要完成網(wǎng)絡(luò)層的操作.創(chuàng)建一個名為Network的group.在此group中我們將創(chuàng)建兩個類.一個是AFHTTPSessionManager的子類,叫做SessionManager,它是AFNetworking中的一個session manager類,一個非常好用的網(wǎng)絡(luò)庫.SessionManager類將用單例來處理API的請求.我們創(chuàng)建后,拷貝下面的代碼到相應(yīng)地接口和實(shí)現(xiàn)代碼中.
#import "AFHTTPSessionManager.h"
@interface SessionManager : AFHTTPSessionManager
+ (id)sharedManager;
@end
#import "SessionManager.h"
static NSString *const kBaseURL = @"http://api.nytimes.com";
@implementation SessionManager
- (id)init {
self = [super initWithBaseURL:[NSURL URLWithString:kBaseURL]];
if(!self) return nil;
self.responseSerializer = [AFJSONResponseSerializer serializer];
self.requestSerializer = [AFJSONRequestSerializer serializer];
return self;
}
+ (id)sharedManager {
static SessionManager *_sessionManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_sessionManager = [[self alloc] init];
});
return _sessionManager;
}
@end
Session manager初始化了一個靜態(tài)變量kBaseURL作為我們的基礎(chǔ)URL.將用JSON格式的數(shù)據(jù)作為請求和接收的數(shù)據(jù)格式類型.
- 紐約時報搜索文章API預(yù)覽
官方文檔在這里,我們將用到以下接口:
http://api.nytimes.com/svc/search/v2/articlesearch
...解析這些文章需要一個日期范圍的搜索查詢.例如,我們要查詢2015年7月第一周紐約時報中有關(guān)籃球的文章.根據(jù)接口文檔,我們需要以下參數(shù):
| Parameter | Value |
| -------------- |:--------------- :|
| q | "basketBall" |
| begin_date | "20150701" |
| begin_date | "20150707" |
返回的數(shù)據(jù)稍微有些復(fù)雜.下面是返回的數(shù)據(jù)中的其中一篇文章的數(shù)據(jù)(docs array中的一個元素)
{
"response": {
"docs": [
{
"web_url": "http://www.nytimes.com/2015/07/04/sports/basketball/robin-lopez-and-knicks-are-close-to-a-deal.html",
"lead_paragraph": "Lopez, a 7-foot center, joined Arron Afflalo, a 6-foot-5 guard, as the Knicks’ key acquisitions in free agency. He is expected to solidify the Knicks’ interior defense.",
"abstract": null,
"print_page": "1",
"source": "The New York Times",
"pub_date": "2015-07-04T00:00:00Z",
"document_type": "article",
"news_desk": "Sports",
"section_name": "Sports",
"subsection_name": "Pro Basketball",
"type_of_material": "News",
"_id": "5596e7ac38f0d84c0655cb28",
"word_count": "879"
}
]
},
"status": "OK",
"copyright": "Copyright (c) 2013 The New York Times Company. All Rights Reserved."
}
返回的數(shù)據(jù)中分為三個部分.第一部分response是docs數(shù)組的包含文章的內(nèi)容.其他兩部分是狀態(tài)和版權(quán)信息.API已經(jīng)請求完成,現(xiàn)在需要用Mantle來創(chuàng)建數(shù)據(jù)模型了.
-
Mantle介紹
如前文所述,Mantle是簡化數(shù)據(jù)模型轉(zhuǎn)換的開源庫.我們先來創(chuàng)建一個文章列表的請求模型.我們命名這個類為ArticleListRequestModel,它是MTLModel的子類,所有Mantle模型都繼承此類.另外我們需要它遵從MTLJSONSerializing協(xié)議.我們的請求模型里有三個請求參數(shù),分別為query,articlesFromDate和articlesToDate.確保這個類在Models group里面.Mantle能簡化數(shù)據(jù)模型,減少代碼量
這里是ArticleListRequestModel接口中的內(nèi)容:
#import "MTLModel.h"
#import "Mantle.h"
@interface ArticleListRequestModel : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy) NSString *query;
@property (nonatomic, copy) NSDate *articlesFromDate;
@property (nonatomic, copy) NSDate *articlesToDate;
@end
如果你查看我們的請求參數(shù)與docs的請求參數(shù),會發(fā)現(xiàn)API中的請求參數(shù)名和我們模型中的名字有所不同.Mantle用以下方法來解決:
+ (NSDictionary *)JSONKeyPathsByPropertyKey.
下面是請求模型文件的實(shí)現(xiàn)代碼中需要做的:
#import "ArticleListRequestModel.h"
@implementation ArticleListRequestModel
#pragma mark - Mantle JSONKeyPathsByPropertyKey
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"query": @"q",
@"articlesFromDate": @"begin_date",
@"articlesToDate": @"end_date"
};
}
@end
這個實(shí)現(xiàn)方法實(shí)現(xiàn)了將JSON數(shù)據(jù)轉(zhuǎn)換為模型屬性.一旦JSONKeyPathByPropertyKey方法實(shí)現(xiàn),偶們就能用類方法+[MTLJSONAdapter JSONArrayForModels]將模型轉(zhuǎn)為JSON詞典.
有件事需要注意的是我們參數(shù)列表中的日期參數(shù)需要是"YYYYMMDD"格式.用Mantle很容易做到.我們需要增加一個自定義的屬性值轉(zhuǎn)換可選方法'+<propertyName>JSONTransformer'.通過這個方法來告訴Mantle在解析JSON數(shù)據(jù)的時候需要單獨(dú)做出一些轉(zhuǎn)變方法.我們也會實(shí)現(xiàn)反轉(zhuǎn)方法用于創(chuàng)建模型轉(zhuǎn)JSON的方法中.如果我們要將NSDate對象轉(zhuǎn)為String,則需要用到NSDataFormatter類.下面是ArtcleListRequestModel類的實(shí)現(xiàn)代碼:
#import "ArticleListRequestModel.h"
@implementation ArticleListRequestModel
+ (NSDateFormatter *)dateFormatter {
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"yyyyMMdd";
return dateFormatter;
}
#pragma mark - Mantle JSONKeyPathsByPropertyKey
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"query": @"q",
@"articlesFromDate": @"begin_date",
@"articlesToDate": @"end_date"
};
}
#pragma mark - JSON Transformers
+ (NSValueTransformer *)articlesToDateJSONTransformer {
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];
}];
}
+ (NSValueTransformer *)articlesFromDateJSONTransformer {
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];
}];
}
@end
Mantle另一個非常棒的特性是這些模型全部遵從NSCoding協(xié)議,和實(shí)現(xiàn)isEqual和hash方法.
我們可以看到API返回的JSON數(shù)據(jù)包含了一個文章對象的數(shù)組.如果我們想要用Mantle來模型化這個返回值,需要創(chuàng)建獨(dú)立的兩個models.一個用來模型化文章集的對象(docs數(shù)組中的元素),另一個用來模型化整個JSON返回對象中包含docs數(shù)組的部分.現(xiàn)在,我們無需解析從接收的JSON數(shù)據(jù)到我們數(shù)據(jù)模型中的每個嵌套元素.假若我們只對文章對象的兩部分lead_paragraph和web_url感興趣.ArticleModel類很簡單的就能實(shí)現(xiàn),代碼如下:
#import "MTLModel.h"
#import <Mantle/Mantle.h>
@interface ArticleModel : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy) NSString *leadParagraph;
@property (nonatomic, copy) NSString *url;
@end
#import "ArticleModel.h"
@implementation ArticleModel
#pragma mark - Mantle JSONKeyPathsByPropertyKey
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"leadParagraph": @"lead_paragraph",
@"url": @"web_url"
};
}
@end
現(xiàn)在文章的model已經(jīng)被定義,我們能夠?qū)崿F(xiàn)對返回模型中文章列表對象的模型化.下面是ArticleList返回模型的代碼:
#import "MTLModel.h"
#import <Mantle/Mantle.h>
#import "ArticleModel.h"
@interface ArticleListResponseModel : MTLModel <MTLJSONSerializing>
@property (nonatomic, copy) NSArray *articles;
@property (nonatomic, copy) NSString *status;
@end
#import "ArticleListResponseModel.h"
@class ArticleModel;
@implementation ArticleListResponseModel
#pragma mark - Mantle JSONKeyPathsByPropertyKey
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@"articles" : @"response.docs",
@"status" : @"status"
};
}
#pragma mark - JSON Transformer
+ (NSValueTransformer *)articlesJSONTransformer {
return [MTLJSONAdapter arrayTransformerWithModelClass:ArticleModel.class];
}
@end
這個類只有兩個屬性:status和articles.如果我們同endpoint的返回值來比較,就會發(fā)現(xiàn)第三個JSON屬性版權(quán)沒有轉(zhuǎn)換到返回值模型中.在articlesJSONTransformer方法中,我們會看到它返回的是ArticleModel類的數(shù)組.
值得注意的是在JSONKeyPathsByPropertyKey方法里,模型屬性文章集相當(dāng)于JSON屬性response里的docs數(shù)組.
截止現(xiàn)在,我們實(shí)現(xiàn)了三個model類:ArticleListRequestModel,ArticleModel,和ArticleLsitResponseModel.
- 第一個API請求

我們實(shí)現(xiàn)了所有數(shù)據(jù)模型,現(xiàn)在該返回APIManager類來用GET方法實(shí)現(xiàn)API請求.方法為:
- (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure
ArticleListRequestModel請求模型作為參數(shù),ArticleListResponseModel模型作為返回成功后的結(jié)果,不成功時返回一個NSError信息.我們使用AFNetworking來實(shí)現(xiàn)API的GET請求方法.請確保你已經(jīng)有了前文所說的api access key,在這里獲取.
#import "SessionManager.h"
#import "ArticleListRequestModel.h"
#import "ArticleListResponseModel.h"
@interface APIManager : SessionManager
- (NSURLSessionDataTask *)getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure;
@end
#import "APIManager.h"
#import "Mantle.h"
static NSString *const kArticlesListPath = @"/svc/search/v2/articlesearch.json";
static NSString *const kApiKey = @"replace this with your own key";
@implementation APIManager
- (NSURLSessionDataTask *)getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel
success:(void (^)(ArticleListResponseModel *responseModel))success
failure:(void (^)(NSError *error))failure{
NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil];
NSMutableDictionary *parametersWithKey = [[NSMutableDictionary alloc] initWithDictionary:parameters];
[parametersWithKey setObject:kApiKey forKey:@"api-key"];
return [self GET:kArticlesListPath parameters:parametersWithKey
success:^(NSURLSessionDataTask *task, id responseObject) {
NSDictionary *responseDictionary = (NSDictionary *)responseObject;
NSError *error;
ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class
fromJSONDictionary:responseDictionary error:&error];
success(list);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
failure(error);
}];
}
有兩件重要的事情在這個方法里,首先我們來看這行代碼:
NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil];
這行代碼利用MTLJSONAdapater類中提供的方法來獲取一個從我們數(shù)據(jù)模型中轉(zhuǎn)換來的NSDictionary.它將隨JSON一起發(fā)送給API.這就是Mantle干的漂亮的地方.僅用一行代碼就實(shí)現(xiàn)了ArticleListRequestModel類中的JSONKeyPathsByPropertyKey和+<propertyName>JSONTransformer方法,獲取了我們數(shù)據(jù)模型所需的JSON數(shù)據(jù).
Mantle還可以允許我們用同樣方法來解析從API獲得的數(shù)據(jù).將獲取的數(shù)據(jù)詞典轉(zhuǎn)換為ArticleListResponseModel類只需下面的方法:
ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error];
- 用Realm來存儲數(shù)據(jù)
現(xiàn)在我們已經(jīng)可以從遠(yuǎn)程API來解析數(shù)據(jù),是時候來存儲它們了.如前文所述,我們將用到Realm.Realm是用來替代Core Data和SQLite的移動數(shù)據(jù)庫.正如下面所述,它非常易用:
Realm, the ultimate mobile database, is a perfect replacement for Core Data and SQLite.
用Realm來存儲數(shù)據(jù)之前,我們需要創(chuàng)建一個RLMObject類的子類.創(chuàng)建一個存儲單篇文章的模型類.代碼如下,相當(dāng)簡單:
#import "RLMObject.h"
@interface ArticleRealm : RLMObject
@property NSString *leadParagraph;
@property NSString *url;
@end
這個是存儲的基礎(chǔ),而類的實(shí)現(xiàn)代碼為空.確保你寫的model類中的屬性沒有其他例如nonatomic、Strong或者copy的描述.Realm會自動完成,無需我們關(guān)心.
我們得到的文章集利用Mantle轉(zhuǎn)換為Article模型后,需要初始化ArticleRealm對象中的文章類.為了做到這些,需要在Realm模型中增加initWithMantleModel方法.下面是ArticleRealm類的實(shí)現(xiàn)代碼:
#import "RLMObject.h"
#import "ArticleModel.h"
@interface ArticleRealm : RLMObject
@property NSString *leadParagraph;
@property NSString *url;
- (id)initWithMantleModel:(ArticleModel *)articleModel;
@end
#import "ArticleRealm.h"
@implementation ArticleRealm
- (id)initWithMantleModel:(ArticleModel *)articleModel{
self = [super init];
if(!self) return nil;
self.leadParagraph = articleModel.leadParagraph;
self.url = articleModel.url;
return self;
}
@end
我們用RLMRealm類來進(jìn)行數(shù)據(jù)庫中的數(shù)據(jù)交互.我們通過"[RLMRealm defaultRealm]"很容易就能得到一個RLMRealm對象.這個對象是線程安全的.往Realm寫入數(shù)據(jù)相當(dāng)簡單明了.一次寫入或者一系列的寫入需要一個寫入轉(zhuǎn)換操作.下面是寫入操作的例子:
RLMRealm *realm = [RLMRealm defaultRealm];
ArticleRealm *articleRealm = [ArticleRealm new];
articleRealm.leadParagraph = @"abc";
articleRealm.url = @"sampleUrl";
[realm beginWriteTransaction];
[realm addObject:articleRealm];
[realm commitWriteTransaction];
以上操作包含如下步驟:首先創(chuàng)建一個RLMRealm對象來與數(shù)據(jù)庫交互.然后創(chuàng)建一個ArticleRealm模型對象(確保它是RLMRealm的子類).最后存儲它,進(jìn)行一個寫入操作,這個對象被添加到數(shù)據(jù)庫,完成存儲后寫入操作結(jié)束.我們可以看到寫入操作會阻塞相應(yīng)的線程.雖然Realm很快,但是如果我們在主線程中做很多的寫入數(shù)據(jù)操作,UI就會被掛起,直到寫入操作結(jié)束.一個好的解決辦法就是將寫入操作放在子線程中完成.
- Realm中的API 請求和對返回數(shù)據(jù)的存儲
接下來我們利用Realm來存儲文章集的所有信息.首先用下列方法來做一個API請求:
- (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure
Mantle獲取紐約時報發(fā)表于2015年7月頭一周關(guān)于籃球文章集的請求和返回數(shù)據(jù)模型還沒沒有用到.一旦有文章獲取時,我們便將它們存儲在Realm里.下面的代碼做的就是這些工作.在我們的app table view controller類的viewDidLoad方法里編寫如下代碼:
ArticleListRequestModel *requestModel = [ArticleListRequestModel new]; // (1)
requestModel.query = @"Basketball";
requestModel.articlesToDate = [[ArticleListRequestModel dateFormatter] dateFromString:@"20150706"];
requestModel.articlesFromDate = [[ArticleListRequestModel dateFormatter] dateFromString:@"20150701"];
[[APIManager sharedManager] getArticlesWithRequestModel:requestModel // (2)
success:^(ArticleListResponseModel *responseModel){
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // (3)
@autoreleasepool {
RLMRealm *realm = [RLMRealm defaultRealm];
[realm beginWriteTransaction];
[realm deleteAllObjects];
[realm commitWriteTransaction];
[realm beginWriteTransaction];
for(ArticleModel *article in responseModel.articles){
ArticleRealm *articleRealm = [[ArticleRealm alloc] initWithMantleModel:article]; // (4)
[realm addObject:articleRealm];
}
[realm commitWriteTransaction];
dispatch_async(dispatch_get_main_queue(), ^{ // (5)
RLMRealm *realmMainThread = [RLMRealm defaultRealm]; // (6)
RLMResults *articles = [ArticleRealm allObjectsInRealm:realmMainThread];
self.articles = articles; // (7)
[self.tableView reloadData];
});
}
});
} failure:^(NSError *error) {
self.articles = [ArticleRealm allObjects];
[self.tableView reloadData];
}];
首先,API利用請求model(1)調(diào)用(2),返回的模型里包含文章的列表.為了在Realm里存儲這些文章,我們需要在循環(huán)(4)里創(chuàng)建一個Realm對象.多個對象的寫入,需要在子線程中進(jìn)行操作(3).現(xiàn)在所有文章都存儲在了Realm中,我們將它們賦值于類的self.articles(7)里.稍后我們將要在主線程的TableView的datasource方法里使用.從主線程中取出Realm中的數(shù)據(jù)是安全的(5).在新的線程中通過創(chuàng)建新的RLMRealm對象(6)來讀取數(shù)據(jù)庫.

如果從API中獲取最新的文章失敗,就會從failure block中讀取本地數(shù)據(jù).
- 后記
這篇教程中我們學(xué)到了如何使用Mantle(Cocoa和Cocoa Touch的模型庫),來同遠(yuǎn)程API數(shù)據(jù)交互.我們還學(xué)到了使用Realm數(shù)據(jù)庫來存儲Mantle模型對象.
如果你想看看這個程序,可以從這里下載代碼.運(yùn)行前,需要替換相關(guān)的API key.
我覺得我需要一個校對??
Girl學(xué)iOS100天 第6天