Упрощение использования RESTful API и сохранения данных на iOS с помощью Mantle и Realm

Опубликовано: 2022-03-11

Каждый разработчик iOS знаком с Core Data, фреймворком объектного графа и персистентности от Apple. Помимо локального сохранения данных, платформа имеет множество дополнительных функций, таких как отслеживание изменений объектов и отмена. Эти функции, хотя и полезные во многих случаях, предоставляются не бесплатно. Для этого требуется много стандартного кода, а фреймворк в целом требует крутой кривой обучения.

В 2014 году была выпущена мобильная база данных Realm, которая покорила мир разработчиков. Если все, что нам нужно, — это сохранять данные локально, Realm — хорошая альтернатива. В конце концов, не все варианты использования требуют расширенных функций Core Data. Realm чрезвычайно прост в использовании и, в отличие от Core Data, требует очень небольшого шаблонного кода. Он также является потокобезопасным и, как говорят, работает быстрее, чем фреймворк сохранения от Apple.

В большинстве современных мобильных приложений сохранение данных решает половину проблемы. Нам часто нужно получать данные из удаленной службы, обычно через RESTful API. Здесь в игру вступает Mantle. Это модельная платформа с открытым исходным кодом для Cocoa и Cocoa Touch. Mantle значительно упрощает написание моделей данных для взаимодействия с API, которые используют JSON в качестве формата обмена данными.

Realm и Mantle для iOS

В этой статье мы создадим приложение для iOS, которое извлекает список статей вместе со ссылками на них из API поиска статей New York Times версии 2. Список будет получен с использованием стандартного HTTP-запроса GET с моделями запросов и ответов, созданными с помощью Mantle. Мы увидим, как легко с помощью Mantle обрабатывать преобразования значений (например, из NSDate в строку). Как только данные будут получены, мы сохраним их локально с помощью Realm. Все это с минимальным шаблонным кодом.

RESTful API — Начало работы

Давайте начнем с создания нового проекта Xcode «Приложение Master-Detail» для iOS с именем «RealmMantleTutorial». Мы будем добавлять к нему фреймворки с помощью CocoaPods. Подфайл должен выглядеть следующим образом:

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

После установки модулей мы можем открыть только что созданное рабочее пространство MantleRealmTutorial . Как вы заметили, также был установлен знаменитый фреймворк AFNetworking. Мы будем использовать его для выполнения запросов к API.

Как упоминалось во введении, New York Times предоставляет отличный 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

Менеджер сеансов инициализируется базовым URL-адресом, определенным в статической переменной kBaseURL . Он также будет использовать сериализаторы запросов и ответов JSON.

Теперь второй класс, который мы собираемся создать в группе Network , будет называться APIManager . Он должен быть получен из нашего вновь созданного класса SessionManager . После создания необходимых моделей данных мы добавим в ApiManager метод, который будет использоваться для запроса списка статей из API.

Обзор API поиска статей New York Times

Официальная документация по этому превосходному API доступна по адресу http://developer.nytimes.com/…/article_search_api_v2. Мы собираемся использовать следующую конечную точку:

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

… для получения статей, найденных с использованием выбранного нами условия поискового запроса, ограниченного диапазоном дат. Например, мы можем попросить API вернуть список всех статей, появившихся в New York Times и имеющих какое-либо отношение к баскетболу за первые семь дней июля 2015 года. Согласно документации API, для этого нам нужно установить следующие параметры в запросе на получение этой конечной точки:

Параметр Стоимость
д "баскетбол"
begin_date «20150701»
Дата окончания «20150707»

Ответ от API довольно сложный. Ниже приведен ответ на запрос с вышеуказанными параметрами, ограниченный только одной статьей (один элемент в массиве документов) с опущенными для ясности многочисленными полями.

 { "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 , который, в свою очередь, содержит элементы, представляющие статьи. Два других поля — статус и авторские права . Теперь, когда мы знаем, как работает API, пришло время создать модели данных с помощью Mantle.

Введение в мантию

Как упоминалось ранее, Mantle — это фреймворк с открытым исходным кодом, который значительно упрощает написание моделей данных. Начнем с создания модели запроса списка статей. Давайте назовем этот класс ArticleListRequestModel и убедимся, что он является производным от MTLModel , от которого должны быть получены все модели Mantle. Дополнительно давайте сделаем его соответствующим протоколу MTLJSONSerializing . Наша модель запроса должна иметь три свойства подходящих типов: query, articleFromDate и articleToDate . Просто чтобы убедиться, что наш проект хорошо организован, я предлагаю поместить этот класс в группу « Модели ».

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 мы можем получить представление модели в словаре JSON с помощью метода класса +[MTLJSONAdapter JSONArrayForModels:] .

Одна вещь, которая все еще остается, как мы знаем из списка параметров, заключается в том, что оба параметра даты должны быть в формате «ГГГГММДД». Вот где 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 , а также реализуют методы isEqual и hash .

Как мы уже видели, результирующий JSON из вызова API содержит массив объектов, представляющих статьи. Если мы хотим смоделировать этот ответ с помощью Mantle, нам придется создать две отдельные модели данных. Один будет моделировать объекты, представляющие статьи (элементы массива документов ), а другой будет моделировать весь ответ JSON, за исключением элементов массива документов. Теперь нам не нужно сопоставлять каждое свойство из входящего JSON с нашими моделями данных. Предположим, что нас интересуют только два поля объектов статьи, и это будут lead_paragraph и web_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

Этот класс имеет только два свойства: статус и статьи . Если мы сравним его с ответом от конечной точки, мы увидим, что третий атрибут JSON copyright не будет отображаться в модели ответа. Если мы посмотрим на метод articleJSONTransformer , то увидим, что он возвращает преобразователь значений для массива, содержащего объекты класса ArticleModel .

Также стоит отметить, что в методе JSONKeyPathsByPropertyKey статьи свойств модели соответствуют документам массива, вложенным в ответ атрибута JSON.

К настоящему времени у нас должно быть реализовано три класса моделей: ArticleListRequestModel, ArticleModel и ArticleListResponseModel.

Первый запрос API

Спокойный API

Теперь, когда мы реализовали все модели данных, пришло время вернуться к классу APIManager , чтобы реализовать метод, который мы будем использовать для выполнения GET-запросов к API. Метод:

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

принимает модель запроса ArticleListRequestModel в качестве параметра и возвращает ArticleListResponseModel в случае успеха или NSError в противном случае. Реализация этого метода использует AFNetworking для выполнения запроса GET к API. Обратите внимание, что для успешного выполнения запроса 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 . Это представление отражает JSON, который будет отправлен в API. В этом и заключается красота Мантии. Реализовав методы JSONKeyPathsByPropertyKey и +<propertyName>JSONTransformer в классе ArticleListRequestModel, мы можем быстро получить правильное JSON-представление нашей модели данных с помощью всего одной строки кода.

Мантия также позволяет нам выполнять преобразования и в другом направлении. Именно это и происходит с данными, полученными от 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

И это могло бы быть в принципе так, реализация этого класса могла бы остаться пустой. Обратите внимание, что свойства в классе модели не имеют таких атрибутов, как nonatomic, strong или copy. Realm позаботится о них, и нам не нужно о них беспокоиться.

Поскольку статьи, которые мы можем получить, моделируются моделью Mante Article , было бы удобно инициализировать объекты ArticleRealm объектами класса Article . Для этого мы добавим метод 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 , вызвав метод «[RLMRealm defaultRealm]». Важно помнить, что такой объект действителен только в пределах потока, в котором он был создан, и не может быть разделен между потоками. Запись данных в 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 ). Наконец, чтобы сохранить его, начинается транзакция записи, объект добавляется в базу данных, и после его сохранения транзакция записи фиксируется. Как мы видим, транзакции записи блокируют поток, в котором они вызываются. Хотя говорят, что Realm работает очень быстро, если бы мы добавили несколько объектов в базу данных в рамках одной транзакции в основном потоке, это могло бы привести к тому, что пользовательский интерфейс перестал отвечать до тех пор, пока транзакция не будет завершена. Естественным решением этого является выполнение такой транзакции записи в фоновом потоке.

Запрос API и постоянный ответ в Realm

Это вся информация, которая нам нужна для сохранения статей с помощью Realm. Попробуем выполнить API-запрос методом

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

и модели запросов и ответов Mantle, чтобы получить статьи New York Times, которые имели какое-либо отношение (как в предыдущем примере) к баскетболу и были опубликованы в первые семь дней июня 2015 года. Как только список таких статей станет доступен, мы сохранит его в 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]; }];

Сначала выполняется вызов API (2) с моделью запроса (1), которая возвращает модель ответа, содержащую список статей. Чтобы сохранить эти статьи с помощью Realm, нам нужно создать объекты модели Realm, что происходит в цикле for (4). Также важно отметить, что, поскольку несколько объектов сохраняются в рамках одной транзакции записи, эта транзакция записи выполняется в фоновом потоке (3). Теперь, когда все статьи сохранены в Realm, мы назначаем их свойству класса self.articles (7). Поскольку доступ к ним будет осуществляться позже в основном потоке в методах источника данных TableView, их также безопасно извлекать из базы данных Realm в основном потоке (5). Опять же, чтобы получить доступ к базе данных из нового потока, необходимо создать новый объект RLMRealm (6) в этом потоке.

Если получить новые статьи из API по какой-либо причине не удается, существующие извлекаются из локального хранилища в блоке отказа.

Подведение итогов

В этом руководстве мы узнали, как настроить Mantle, фреймворк модели для Cocoa и Cocoa Touch, для взаимодействия с удаленным API. Мы также узнали, как локально сохранять данные, полученные в виде объектов модели Mantle, с помощью мобильной базы данных Realm.

Если вы хотите попробовать это приложение, вы можете получить исходный код из репозитория GitHub. Вам нужно будет сгенерировать и предоставить свой собственный ключ API перед запуском приложения.