MantleとRealmを使用したiOSでのRESTfulAPIの使用とデータの永続性の簡素化

公開: 2022-03-11

すべてのiOS開発者は、Appleのオブジェクトグラフおよび永続性フレームワークであるCoreDataに精通しています。 データをローカルに永続化する以外に、フレームワークには、オブジェクトの変更の追跡や元に戻すなど、多数の高度な機能が付属しています。 これらの機能は多くの場合便利ですが、無料では提供されません。 それは多くの定型コードを必要とし、フレームワークは全体として急な学習曲線を持っています。

2014年には、モバイルデータベースであるRealmがリリースされ、開発の世界を席巻しました。 データをローカルに永続化するだけでよい場合は、レルムが適切な代替手段です。 結局のところ、すべてのユースケースがコアデータの高度な機能を必要とするわけではありません。 Realmは非常に使いやすく、Core Dataとは対照的に、ボイラープレートコードはほとんど必要ありません。 また、スレッドセーフであり、Appleの永続性フレームワークよりも高速であると言われています。

最新のモバイルアプリケーションのほとんどでは、データを永続化することで問題の半分を解決できます。 多くの場合、通常はRESTful APIを介して、リモートサービスからデータをフェッチする必要があります。 ここでマントルが活躍します。 これは、CocoaおよびCocoaTouchのオープンソースモデルフレームワークです。 Mantleは、データ交換形式としてJSONを使用するAPIと対話するためのデータモデルの記述を大幅に簡素化します。

iOS用のレルムとマントル

この記事では、New York Times Article SearchAPIv2から記事のリストとそれらへのリンクを取得するiOSアプリケーションを構築します。 リストは、標準のHTTP GET要求を使用してフェッチされ、要求モデルと応答モデルはMantleを使用して作成されます。 Mantleを使用すると、値の変換(NSDateから文字列への変換など)を簡単に処理できることがわかります。 データがフェッチされたら、Realmを使用してローカルに永続化します。 最小限の定型コードでこれらすべて。

RESTfulAPI-はじめに

「RealmMantleTutorial」という名前のiOS用の新しい「Master-DetailApplication」Xcodeプロジェクトを作成することから始めましょう。 CocoaPodsを使用してフレームワークを追加します。 podfileは次のようになります。

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

ポッドをインストールしたら、新しく作成したMantleRealmTutorialワークスペースを開くことができます。 お気づきのように、有名なAFNetworkingフレームワークもインストールされています。 これを使用して、APIへのリクエストを実行します。

冒頭で述べたように、NewYorkTimesは優れた記事検索APIを提供します。 これを使用するには、APIへのアクセスキーを取得するためにサインアップする必要があります。 これはhttp://developer.nytimes.comで行うことができます。 APIキーが手元にあれば、コーディングを開始する準備が整います。

Mantleデータモデルの作成を検討する前に、ネットワーク層を稼働させる必要があります。 Xcodeで新しいグループを作成し、それをネットワークと呼びましょう。 このグループでは、2つのクラスを作成します。 最初のSessionManagerを呼び出して、それが楽しいネットワーキングフレームワークであるAFNetworkingのセッションマネージャークラスであるAFHTTPSessionManagerから派生していることを確認しましょう。 SessionManagerクラスは、APIへのgetリクエストを実行するために使用するシングルトンオブジェクトになります。 クラスが作成されたら、以下のコードをそれぞれインターフェースファイルと実装ファイルにコピーしてください。

 #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グループに作成する2番目のクラスはAPIManagerと呼ばれます。 これは、新しく作成されたSessionManagerクラスから派生します。 必要なデータモデルが作成されたら、APIから記事のリストをリクエストするために使用されるメソッドをApiManagerに追加します。

NewYorkTimesの記事検索APIの概要

この優れたAPIの公式ドキュメントは、http://developer.nytimes.com/…/article_search_api_v2で入手できます。 これから行うことは、次のエンドポイントを使用することです。

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

…日付範囲で囲まれた、選択した検索クエリ用語を使用して見つかった記事をフェッチします。 たとえば、2015年7月の最初の7日間にバスケットボールに関係したニューヨークタイムズに掲載されたすべての記事のリストを返すようにAPIに依頼することができます。APIドキュメントによると、これを行うにはそのエンドポイントへのgetリクエストで次のパラメータを設定する必要があります。

パラメータ価値
q "バスケットボール"
begin_date 「20150701」
終了日「20150707」

APIからの応答は非常に複雑です。 以下は、上記のパラメーターが1つの記事(docs配列の1つのアイテム)に限定され、わかりやすくするために多数のフィールドが省略されているリクエストに対する応答です。

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

基本的に私たちが答えるのは3つの分野です。 最初に応答と呼ばれるものには配列docsが含まれ、配列docsには記事を表すアイテムが含まれます。 他の2つのフィールドは、ステータス著作権です。 APIがどのように機能するかがわかったので、Mantleを使用してデータモデルを作成します。

マントル入門

前述のように、Mantleはデータモデルの記述を大幅に簡素化するオープンソースフレームワークです。 記事リストリクエストモデルを作成することから始めましょう。 このクラスをArticleListRequestModelと呼び、すべてのMantleモデルの派生元となるクラスであるMTLModelから派生していることを確認しましょう。 さらに、 MTLJSONSerializingプロトコルに準拠させましょう。 リクエストモデルには、適切なタイプの3つのプロパティ(query、 articlesFromDatearticlesToDate )が必要です。 プロジェクトが適切に構成されていることを確認するために、このクラスをモデルグループに配置することをお勧めします。

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ディクショナリ表現を取得できます。

パラメータのリストからわかるように、まだ残っていることの1つは、両方の日付パラメータが「YYYYMMDD」の形式である必要があるということです。 これは、マントルが非常に便利になる場所です。 オプションのメソッド+<propertyName>JSONTransformerを実装することで、任意のプロパティにカスタム値変換を追加できます。 これを実装することで、JSONデシリアライズ中に特定のJSONフィールドの値をどのように変換するかをMantleに指示します。 モデルから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のもう1つの優れた機能は、これらのモデルがすべてNSCodingプロトコルに準拠し、 isEqualメソッドとハッシュメソッドを実装していることです。

すでに見てきたように、API呼び出しから得られるJSONには、記事を表すオブジェクトの配列が含まれています。 Mantleを使用してこの応答をモデル化する場合は、2つの別個のデータモデルを作成する必要があります。 1つは記事を表すオブジェクト( docs配列要素)をモデル化し、もう1つはdocs配列の要素を除くJSON応答全体をモデル化します。 これで、着信JSONのすべてのプロパティをデータモデルにマッピングする必要がなくなりました。 記事オブジェクトの2つのフィールドのみに関心があり、それらはlead_paragraphweb_urlであると仮定します。 以下に示すように、 ArticleModelクラスの実装はかなり簡単です。

 #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の2つのプロパティしかありません。 これをエンドポイントからの応答と比較すると、3番目のJSON属性の著作権が応答モデルにマップされていないことがわかります。 articlesJSONTransformerメソッドを見ると、 ArticleModelクラスのオブジェクトを含む配列の値トランスフォーマーが返されることがわかります。

また、メソッドJSONKeyPathsByPropertyKeyでは、モデルプロパティの記事がJSON属性応答内にネストされている配列ドキュメントに対応していることにも注意してください。

これで、ArticleListRequestModel、ArticleModel、ArticleListResponseModelの3つのモデルクラスが実装されているはずです。

最初のAPIリクエスト

Restful 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); }]; }

このメソッドの実装では、2つの非常に重要なことが起こります。 まず、この行を見てみましょう。

 NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil];

ここで起こっていることは、 MTLJSONAdapterクラスによって提供されるメソッドを使用して、データモデルのNSDictionary表現を取得することです。 この表現は、APIに送信されるJSONを反映しています。 ここにマントルの美しさがあります。 ArticleListRequestModelクラスにJSONKeyPathsByPropertyKeyメソッドと+<propertyName>JSONTransformerメソッドを実装すると、1行のコードでデータモデルの正しいJSON表現をすぐに取得できます。

マントルはまた、私たちが他の方向にも変換を実行することを可能にします。 そして、それはまさにAPIから受け取ったデータで起こっていることです。 受け取ったNSDictionaryは、次のクラスメソッドを使用してArticleListResponseModelクラスのオブジェクトにマップされます。

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

レルムを使用したデータの永続化

リモートAPIからデータをフェッチできるようになったので、それを永続化するときが来ました。 冒頭で述べたように、レルムを使用して実行します。 Realmはモバイルデータベースであり、CoreDataとSQLiteの代わりになります。 以下に示すように、非常に使いやすいです。

究極のモバイルデータベースであるRealmは、CoreDataとSQLiteの完全な代替品です。
つぶやき

レルムにデータを保存するには、最初にRLMObjectクラスから派生したオブジェクトをカプセル化する必要があります。 ここで行う必要があるのは、単一の記事のデータを格納するモデルクラスを作成することです。 このようなクラスを作成するのは簡単です。

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

そして、これは基本的にそれである可能性があり、このクラスの実装は空のままである可​​能性があります。 モデルクラスのプロパティには、nonatomic、strong、copyなどの属性がないことに注意してください。 レルムがそれらを処理し、私たちはそれらについて心配する必要はありません。

取得できるArticlesはManteモデルArticleでモデル化されているため、 ArticleRealmオブジェクトをArticleクラスのオブジェクトで初期化すると便利です。 これを行うには、 initWithMantleModelメソッドをレルムモデルに追加します。 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のオブジェクトを使用してデータベースと対話します。 メソッド「[RLMRealmdefaultRealm]」を呼び出すことで、 RLMRealmオブジェクトを簡単に取得できます。 このようなオブジェクトは、作成されたスレッド内でのみ有効であり、スレッド間で共有することはできないことを覚えておくことが重要です。 レルムへのデータの書き込みは非常に簡単です。 単一の書き込みまたは一連の書き込みは、書き込みトランザクション内で実行する必要があります。 データベースへの書き込み例は次のとおりです。

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

ここで何が起こるかは次のとおりです。 まず、データベースと対話するためのRLMRealmオブジェクトを作成します。 次に、 ArticleRealmモデルオブジェクトが作成されます(これはRLMRealmクラスから派生していることに注意してください)。 最後にそれを保存するために、書き込みトランザクションが開始され、オブジェクトがデータベースに追加され、保存されると書き込みトランザクションがコミットされます。 ご覧のとおり、書き込みトランザクションは、呼び出されるスレッドをブロックします。 Realmは非常に高速であると言われていますが、メインスレッドの単一のトランザクション内でデータベースに複数のオブジェクトを追加すると、トランザクションが終了するまでUIが応答しなくなる可能性があります。 これに対する自然な解決策は、バックグラウンドスレッドでそのような書き込みトランザクションを実行することです。

レルムでのAPIリクエストと永続的なレスポンス

これは、レルムを使用して記事を永続化するために必要なすべての情報です。 メソッドを使用してAPIリクエストを実行してみましょう

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

バスケットボールと関係があり(前の例のように)、2015年6月の最初の7日間に公開されたニューヨークタイムズの記事を取得するためのMantleリクエストおよびレスポンスモデル。このような記事のリストが利用可能になると、レルムでそれを永続化します。 以下はそれを行うコードです。 これは、アプリのテーブルビューコントローラーの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を使用してこれらの記事を永続化するには、forループ(4)で行われるRealmモデルオブジェクトを作成する必要があります。 複数のオブジェクトが単一の書き込みトランザクション内で永続化されるため、その書き込みトランザクションはバックグラウンドスレッドで実行されることに注意することも重要です(3)。 ここで、すべての記事がレルムに保存されたら、それらをクラスプロパティself.articles (7)に割り当てます。 これらは後でTableViewデータソースメソッドのメインスレッドでアクセスされるため、メインスレッドのレルムデータベースからも安全に取得できます(5)。 この場合も、新しいスレッドからデータベースにアクセスするには、そのスレッドで新しいRLMRealmオブジェクトを作成する必要があります(6)。

APIからの新しい記事の取得が何らかの理由で失敗した場合、既存の記事は失敗ブロックのローカルストレージから取得されます。

まとめ

このチュートリアルでは、リモートAPIと対話するために、CocoaおよびCocoaTouchのモデルフレームワークであるMantleを構成する方法を学習しました。 また、Realmモバイルデータベースを使用して、Mantleモデルオブジェクトの形式で取得されたデータをローカルに永続化する方法も学びました。

このアプリケーションを試してみたい場合は、GitHubリポジトリからソースコードを取得できます。 アプリケーションを実行する前に、独自のAPIキーを生成して提供する必要があります。