Mantle和Realm的使用

本文翻譯自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)換工作.

toptal-blog-image-1437736381161-4aeb77c375aea3e9e8a9eeb692d2aa01.jpg

本文我們將用紐約時報的搜索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,articlesFromDatearticlesToDate.確保這個類在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)isEqualhash方法.
我們可以看到API返回的JSON數(shù)據(jù)包含了一個文章對象的數(shù)組.如果我們想要用Mantle來模型化這個返回值,需要創(chuàng)建獨(dú)立的兩個models.一個用來模型化文章集的對象(docs數(shù)組中的元素),另一個用來模型化整個JSON返回對象中包含docs數(shù)組的部分.現(xiàn)在,我們無需解析從接收的JSON數(shù)據(jù)到我們數(shù)據(jù)模型中的每個嵌套元素.假若我們只對文章對象的兩部分lead_paragraphweb_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

這個類只有兩個屬性:statusarticles.如果我們同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請求

toptal-blog-image-1437736761484-e7c512656ff5de52c619c4c71f298898.jpg

我們實(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ù)庫.

toptal-blog-image-1437736736973-c122aa01a412250edee995b94ce9628c.jpg

如果從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天

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容