使用 Mantle 和 Realm 在 iOS 上簡化 RESTful API 使用和數據持久性

已發表: 2022-03-11

每個 iOS 開發人員都熟悉 Core Data,這是一個來自 Apple 的對像圖和持久性框架。 除了在本地保存數據外,該框架還具有許多高級功能,例如對象更改跟踪和撤消。 這些功能雖然在很多情況下很有用,但並不是免費提供的。 它需要大量樣板代碼,而且整個框架的學習曲線很陡峭。

2014 年,移動數據庫 Realm 發布並席捲了開發世界。 如果我們只需要在本地保存數據,Realm 是一個不錯的選擇。 畢竟,並非所有用例都需要 Core Data 的高級功能。 Realm 非常易於使用,與 Core Data 相比,它只需要很少的樣板代碼。 它也是線程安全的,據說比 Apple 的持久性框架更快。

在大多數現代移動應用程序中,持久化數據解決了一半的問題。 我們經常需要從遠程服務獲取數據,通常是通過 RESTful API。 這就是 Mantle 發揮作用的地方。 它是 Cocoa 和 Cocoa Touch 的開源模型框架。 Mantle 顯著簡化了與使用 JSON 作為數據交換格式的 API 交互的數據模型的編寫。

iOS 的 Realm 和 Mantle

在本文中,我們將構建一個 iOS 應用程序,該應用程序從紐約時報文章搜索 API v2 獲取文章列表以及指向它們的鏈接。 該列表將使用標準 HTTP GET 請求獲取,並使用 Mantle 創建請求和響應模型。 我們將看到使用 Mantle 處理值轉換(例如從 NSDate 到字符串)是多麼容易。 獲取數據後,我們將使用 Realm 將其保存在本地。 所有這些都使用最少的樣板代碼。

RESTful API - 入門

讓我們首先為 iOS 創建一個名為“RealmMantleTutorial”的新“Master-Detail Application”Xcode 項目。 我們將使用 CocoaPods 為其添加框架。 podfile 應類似於以下內容:

 pod 'Mantle' pod 'Realm' pod 'AFNetworking'

安裝 pod 後,我們可以打開新創建的MantleRealmTutorial工作區。 如您所見,著名的 AFNetworking 框架也已安裝。 我們將使用它來執行對 API 的請求。

正如介紹中提到的,紐約時報提供了一個優秀的文章搜索 API。 為了使用它,需要註冊以獲取 API 的訪問密鑰。 這可以在 http://developer.nytimes.com 上完成。 有了 API 密鑰,我們就可以開始編碼了。

在我們深入研究創建 Mantle 數據模型之前,我們需要啟動並運行我們的網絡層。 讓我們在 Xcode 中創建一個新組並將其命名為 Network。 在這個組中,我們將創建兩個類。 讓我們調用第一個SessionManager並確保它是從AFHTTPSessionManager派生的,它是來自令人愉快的網絡框架AFNetworking的會話管理器類。 我們的SessionManager類將是一個單例對象,我們將使用它來執行對 API 的獲取請求。 創建類後,請將以下代碼分別複製到接口和實現文件中。

 #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

會話管理器使用靜態kBaseURL變量中定義的基本 URL 進行初始化。 它還將使用 JSON 請求和響應序列化程序。

現在我們要在Network組中創建的第二個類將被稱為APIManager 。 它應派生自我們新創建的SessionManager類。 一旦創建了必要的數據模型,我們將向ApiManager添加一個方法,該方法將用於從 API 請求文章列表。

紐約時報文章搜索 API 概述

這個優秀 API 的官方文檔可以在 http://developer.nytimes.com/.../article_search_api_v2 找到。 我們要做的是使用以下端點:

 http://api.nytimes.com/svc/search/v2/articlesearch

…獲取使用我們選擇的搜索查詢詞找到的文章,該搜索詞受日期範圍限制。 例如,我們可以要求 API 返回 2015 年 7 月的前 7 天出現在《紐約時報》上的所有與籃球有關的文章的列表。根據 API 文檔,這樣做我們需要在該端點的 get 請求中設置以下參數:

範圍價值
q “籃球”
開始日期“20150701”
結束日期“20150707”

API 的響應非常複雜。 下面是對請求的響應,上述參數僅限於一篇文章(docs 數組中的一項),為清楚起見省略了許多字段。

 { "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." }

我們基本上得到的響應是三個字段。 第一個稱為response包含數組docs ,該數組又包含表示文章的項目。 另外兩個字段是statuscopyright 。 現在我們知道了 API 的工作原理,是時候使用 Mantle 創建數據模型了。

地幔簡介

如前所述,Mantle 是一個開源框架,可顯著簡化數據模型的編寫。 讓我們從創建文章列表請求模型開始。 讓我們調用這個類ArticleListRequestModel並確保它派生自MTLModel ,這是一個所有 Mantle 模型都應該派生的類。 另外讓我們讓它符合MTLJSONSerializing協議。 我們的請求模型應該具有三個合適類型的屬性:query、 articlesFromDatearticleToDate 。 為了確保我們的項目井井有條,我建議將此類放在模型組中。

Mantle 簡化了數據模型的編寫,減少了樣板代碼。
鳴叫

下面是ArticleListRequestModel的接口文件的外觀:

 #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

現在,如果我們查找文章搜索端點的文檔或查看上面的請求參數表,我們會注意到 API 請求中的變量名稱與我們的請求模型中的變量名稱不同。 Mantle 使用以下方法有效地處理這個問題:

 + (NSDictionary *)JSONKeyPathsByPropertyKey.

下面是在我們的請求模型的實現中應該如何實現這個方法:

 #import "ArticleListRequestModel.h" @implementation ArticleListRequestModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"query": @"q", @"articlesFromDate": @"begin_date", @"articlesToDate": @"end_date" }; } @end

此方法的實現指定了模型的屬性如何映射到它的 JSON 表示中。 一旦實現了JSONKeyPathsByPropertyKey方法,我們可以使用類方法+[MTLJSONAdapter JSONArrayForModels:]獲得模型的 JSON 字典表示。

正如我們從參數列表中所知道的,還剩下一件事是兩個日期參數都必須採用“YYYYMMDD”格式。 這是 Mantle 非常方便的地方。 我們可以通過實現可選方法+<propertyName>JSONTransformer為任何屬性添加自定義值轉換。 通過實現它,我們告訴 Mantle 在 JSON 反序列化期間應該如何轉換特定 JSON 字段的值。 我們還可以實現一個可逆轉換器,在從模型創建 JSON 時將使用該轉換器。 由於我們需要將NSDate對象轉換為字符串,我們還將使用NSDataFormatter類。 這是ArticleListRequestModel類的完整實現:

 #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協議,並實現了isEqualhash方法。

正如我們已經看到的,API 調用生成的 JSON 包含一個表示文章的對像數組。 如果我們想使用 Mantle 對這種響應進行建模,我們將不得不創建兩個單獨的數據模型。 一種是對錶示文章的對象( docs數組元素)進行建模,另一種是對整個 JSON 響應進行建模,除了 docs 數組的元素。 現在,我們不必將傳入 JSON 中的每個屬性都映射到我們的數據模型中。 假設我們只對文章對象的兩個字段感興趣,它們是lead_paragraphweb_urlArticleModel類的實現相當簡單,如下所示。

 #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

現在已經定義了文章模型,我們可以通過為文章列表創建模型來完成響應模型定義。 這是類 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 。 如果我們將其與來自端點的響應進行比較,我們將看到第三個 JSON 屬性版權不會映射到響應模型中。 如果我們查看articleJSONTransformer方法,我們會看到它為包含ArticleModel類對象的數組返回一個值轉換器。

還值得注意的是,在JSONKeyPathsByPropertyKey方法中,模型屬性文章對應於嵌套在 JSON 屬性response中的數組文檔。

現在我們應該已經實現了三個模型類:ArticleListRequestModel、ArticleModel 和 ArticleListResponseModel。

第一個 API 請求

寧靜的 API

現在我們已經實現了所有的數據模型,是時候回到APIManager類來實現我們將用來對 API 執行 GET 請求的方法了。 方法:

 - (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure

ArticleListRequestModel請求模型作為參數,如果成功則返回ArticleListResponseModel ,否則返回 NSError。 此方法的實現使用AFNetworking對 API 執行 GET 請求。 請注意,為了發出成功的 API 請求,我們需要提供一個密鑰,如前所述,通過在 http://developer.nytimes.com 上註冊即可獲得該密鑰。

 #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];

這裡發生的是使用MTLJSONAdapter類提供的方法,我們得到了數據模型的NSDictionary表示。 該表示反映了將要發送到 API 的 JSON。 這就是地幔之美的所在。 在 ArticleListRequestModel 類中實現JSONKeyPathsByPropertyKey+<propertyName>JSONTransformer方法後,我們只需一行代碼即可立即獲得數據模型的正確 JSON 表示。

Mantle 還允許我們在另一個方向上執行轉換。 這正是從 API 接收到的數據所發生的情況。 我們收到的 NSDictionary 使用以下類方法映射到 ArticleListResponseModel 類的對像中:

 ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error];

使用 Realm 持久化數據

現在我們能夠從遠程 API 獲取數據,是時候持久化它了。 正如介紹中提到的,我們將使用 Realm 來完成。 Realm 是一個移動數據庫,是 Core Data 和 SQLite 的替代品。 正如我們將在下面看到的,它非常易於使用。

Realm,終極移動數據庫,是 Core Data 和 SQLite 的完美替代品。
鳴叫

要在 Realm 中保存一條數據,我們首先需要封裝一個派生自 RLMObject 類的對象。 我們現在需要做的是創建一個模型類來存儲單篇文章的數據。 這是創建這樣一個類的容易程度。

 #import "RLMObject.h" @interface ArticleRealm : RLMObject @property NSString *leadParagraph; @property NSString *url; @end

基本上就是這樣,這個類的實現可能是空的。 請注意,模型類中的屬性沒有非原子、強或複制等屬性。 Realm 會處理這些問題,我們不必擔心它們。

由於我們可以獲得的文章是使用 Mante 模型Article建模的,因此使用Article類的對像初始化ArticleRealm對象會很方便。 為此,我們將initWithMantleModel方法添加到我們的 Realm 模型中。 這是ArticleRealm類的完整實現。

 #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類的對象與數據庫進行交互。 我們可以通過調用“[RLMRealm defaultRealm]”方法輕鬆獲取RLMRealm對象。 重要的是要記住,這樣的對象僅在創建它的線程內有效,不能跨線程共享。 將數據寫入 Realm 非常簡單。 單個寫入或一系列寫入需要在寫入事務中完成。 這是一個寫入數據庫的示例:

 RLMRealm *realm = [RLMRealm defaultRealm]; ArticleRealm *articleRealm = [ArticleRealm new]; articleRealm.leadParagraph = @"abc"; articleRealm.url = @"sampleUrl"; [realm beginWriteTransaction]; [realm addObject:articleRealm]; [realm commitWriteTransaction];

這裡發生的情況如下。 首先我們創建一個RLMRealm對象來與數據庫交互。 然後創建一個ArticleRealm模型對象(請記住它是從RLMRealm類派生的)。 最後為了保存它,寫事務開始,對像被添加到數據庫中,一旦保存,寫事務就被提交。 正如我們所見,寫事務阻塞了調用它們的線程。 雖然據說 Rea​​lm 非常快,但如果我們要在主線程上的單個事務中將多個對象添加到數據庫中,這可能會導致 UI 在事務完成之前變得無響應。 一個自然的解決方案是在後台線程上執行這樣的寫事務。

領域中的 API 請求和持久響應

這是我們使用 Realm 持久化文章所需的所有信息。 讓我們嘗試使用該方法執行一個 API 請求

- (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure

和 Mantle 請求和響應模型,以獲取與籃球有關的紐約時報文章(如前面的示例)並在 2015 年 6 月的前 7 天發表。一旦此類文章的列表可用,我們將在 Realm 中保留它。 下面是執行此操作的代碼。 它被放置在我們應用程序中表視圖控制器的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]; }];

首先,使用請求模型 (1) 進行 API 調用 (2),該模型返回包含文章列表的響應模型。 為了使用 Realm 持久化這些文章,我們需要創建 Realm 模型對象,這發生在 for 循環 (4) 中。 同樣重要的是要注意,由於多個對像在單個寫入事務中持久化,因此該寫入事務在後台線程 (3) 上執行。 現在,一旦所有文章都保存在 Realm 中,我們將它們分配給類屬性self.articles (7)。 由於稍後將在 TableView 數據源方法中的主線程上訪問它們,因此在主線程上從 Realm 數據庫中檢索它們也是安全的 (5)。 同樣,要從新線程訪問數據庫,需要在該線程上創建 (6) 一個新的 RLMRealm 對象。

如果由於某種原因從 API 獲取新文章失敗,則會從故障塊中的本地存儲中檢索現有文章。

包起來

在本教程中,我們學習瞭如何配置 Mantle,一個用於 Cocoa 和 Cocoa Touch 的模型框架,以便與遠程 API 交互。 我們還學習瞭如何使用 Realm 移動數據庫以 Mantle 模型對象的形式在本地保存檢索到的數據。

如果您想試用此應用程序,可以從其 GitHub 存儲庫中檢索源代碼。 在運行應用程序之前,您需要生成並提供自己的 API 密鑰。