Веб-сервисы, в частности использующие REST-архитектуру, уже плотно вошли в нашу жизнь. Разрабатывая клиентское приложение под iOS, часто так или иначе приходится загружать данные с сервера и хранить/отображать их локально. При этом хочется делать это легко и непринужденно, не прибегая к изобретению собственных “велосипедов”.
Последняя версия известного Objective-C фреймворка RestKit для iOS и OSX значительно упрощает работу с RESTful API. Несомненно, одной из его самых ценных фич является возможность автоматического сохранения объектов в локальную БД, используя CoreData. Давайте вместе проделаем путь от получения данных от сервера до сохранения и отображения их на нашем iOS-устройстве. А чтобы нам не было скучно, в качестве примера будем работать с API всемирно известной компании по производству комиксов Marvel.
Статья представляет из себя некое подобие туториала. Предполагается, что читатель уже знаком с базовыми концепциями разработки на языке Objective-C, использованием iOS SDK, Core Data и такого понятия как блоки.
Для начала давайте зарегистрируемся как разработчик на сайте Marvel.
После тривиальной регистрации переходим на вкладку Account и копируем наши открытый и закрытый ключи.
После этого перейдем на вкладку Interactive Documentation и посмотрим, какие данные нам любезно предоставляют создатели API. У нас есть возможность работать с базой героев, комиксов, создателей, событий и многого другого. Нам же для ознакомления достаточно будет “пощупать” что-то одно, поэтому будущее приложение будет просто загружать список персонажей, сохранять его, а также отображать описание наиболее популярных.
Создадим новый проект в XCode. В качестве устройства выберем iPhone и не забудем оставить галочку возле поля “use Core Data” в окне мастера создания проектов.
Теперь вернемся на портал и рассмотрим структуру объекта
Я специально создал два идентификатора: первый,
Обратите внимание, что я также создал два атрибута
В дальнейшем будет показано, как мы будем работать с этим.
Теперь для правильной работы с сущностями Core Data необходимо также создать Objective-C класс, который будет ее представлять. Создадим класс
Здесь, помимо очевидных соотвествий, появилось свойство
Подключим к нашему проекту RestKit (далее — RK). Как это сделать, подробно расписано здесь (или здесь, если Вы — любитель CocoaPods).
Следующим шагом станет создание класса-обертки
Немного о классах RK.
Не забудьте включить заголовок RK
Наш класс-обертка не будет иметь свойств, но будет иметь две переменных экземпляра:
Давайте рассмотрим, что нам необходимо настроить, чтобы все работало, как надо.
Для начала в
Теперь наши запросы будут отправляться. Что насчет работы с Core Data? Давайте создадим метод, который бы конфигурировал объект типа RKManagedObjectStore.
Последняя строка очень важна. Она связывает между собой два наших главных RK-объекта:
Итак, наша дальнейшая задача — создать в нашем классе
Первая задача реализуется в следующем методе:
Тут нас интересуют несколько параметров у метода
Второе неочевидное значение — это параметр
Теперь понятно, что прежде чем достучаться до собственно списка героев, RK придется пройтись по словарям на несколько уровней вниз, чтобы добраться до нужной структуры. Значение
Вторым методом нашего класса для работы с внутренним объектом RK будет
Обратите внимание, что в коде используется метод из нестандартной категории над
У нашего класса еще будет простой метод
Для начала создадим базовый вью-контроллер
В самом методе включения/выключения анимации будет следующий код:
Теперь, когда мы будем вызывать этот метод со значением
Далее создадим вью-контроллер
В этом вью-контроллере мы будем отображать данные из БД. Для этого будем использовать экземпляр
Как видите, в качестве параметра
После того, как мы добавили маппинг, вызовем метод
Рассмотрим подробно, что он делает:
Сначала мы получаем общее количество персонажей из локальной базы, это значение будет соответствовать количеству ячеек в главной таблице. При первом запуске приложения оно, естественно, будет равняться нулю. Это же значение мы будем использовать в качестве передаваемого параметра offset при обращении к серверу. Таким образом на каждый следующий запрос сервер Marvel будет возвращать только новые объекты героев (по умолчанию герои возвращаются пачками по 20 штук в каждой).
Далее мы производим тот самый главный запрос, используя наш метод-обертку
У этого метода два важных для нас сейчас параметра — это success: и failure:, которые являются блоками, описывающими поведение при успешном и не успешном результатах выполнения запроса соответственно. Итак, при успешном получении массива персонажей, мы генерируем для каждого из них
В случае возникновении какой-то ошибки мы просто выводим ее пользователю и не обновляем таблицу.
В коде используется метод сохранения в хранилище:
Также вы заметите обращение к переменной экземпляра
Ранее в
Как видите, в теле блока, передаваемого в качестве параметра action: мы поместили все тот же метод подгрузки новых героев
Что ж, запустим приложение в эмуляторе и дождемся первого успешного ответа. Если все прошло правильно, и логгер RK вывел что-то наподобие
Для этого зайдем в папку эмулятора, найдем там наше приложение и папку Documents. Там должна находиться база
Ура! У некоторых героев даже есть небольшое описание. Самое время перейти к отображению всего этого «добра».
Я знаю, что нетерпеливый читатель уже давно хочет посмотреть на изображения любимых персонажей. Для этого нам необходимо настроить внешний вид нашей таблицы. Не будем вдаваться в технические подробности создания и настройки объектов типа
После создания очередной ячейки мы достаем нужного героя из базы и отображаем его имя, также мы проверяем, присутствует ли развернутая информация о нем, и помещаем на ячейку кнопку, по нажатию на которую эту информацию потом отобразим. Ну и самое главное — изображение персонажа. Я создал для этого специальный класс
При пустой реализации метода
Давайте реализуем метод загрузки картинки героя. Так как RK уже включает в себя фреймворк
Вот и все. Запустим наше приложение еще раз. Уже хороший результат.
Теперь будет трудно остановиться, и я с вашего позволения использую удобный Pull-To-Refresh контрол для загрузки большего количества персонажей. Заодно проверим, как теперь выглядит наша база.
Теперь и картинки, и информация о героях (естественно только тех, которых мы успели загрузить) будут хранится локально вне зависимости от того, есть у нас соединение с Интернет или нет.
RestKit прекрасно справился с поставленной задачей: запросы отправляются, ответы получаются, объекты сохраняются автоматически. Не всем может понравиться сам принцип загрузки и отображения, предоставленный в этой статье: возможно, что разумнее было бы сразу выкачать всю базу и работать с ней полностью локально. Автор считает, что для ознакомления с базовыми возможностями RK такой функциональности вполне достаточно. Исходный код всего проекта (вместе с недостающей в этой статье частью с отображением информации о конкретном персонаже) можно скачать на GitHub. Ваши пожелания и замечания приветствуются в качестве комментариев к статье, а также пул-реквестов на GitHub.
Напоследок хочется порадовать еще одним изображением — на сей раз это скриншот второго вью-контроллера, который открывается по нажатию на кнопочку “info” возле имени героя в главном вью-контроллере. Уж очень долго я прокручивал свою таблицу, чтоб наконец загрузить его:
Последняя версия известного Objective-C фреймворка RestKit для iOS и OSX значительно упрощает работу с RESTful API. Несомненно, одной из его самых ценных фич является возможность автоматического сохранения объектов в локальную БД, используя CoreData. Давайте вместе проделаем путь от получения данных от сервера до сохранения и отображения их на нашем iOS-устройстве. А чтобы нам не было скучно, в качестве примера будем работать с API всемирно известной компании по производству комиксов Marvel.
Статья представляет из себя некое подобие туториала. Предполагается, что читатель уже знаком с базовыми концепциями разработки на языке Objective-C, использованием iOS SDK, Core Data и такого понятия как блоки.
1. Получаем ключи Marvel и формулируем задачу
Для начала давайте зарегистрируемся как разработчик на сайте Marvel.
После тривиальной регистрации переходим на вкладку Account и копируем наши открытый и закрытый ключи.
После этого перейдем на вкладку Interactive Documentation и посмотрим, какие данные нам любезно предоставляют создатели API. У нас есть возможность работать с базой героев, комиксов, создателей, событий и многого другого. Нам же для ознакомления достаточно будет “пощупать” что-то одно, поэтому будущее приложение будет просто загружать список персонажей, сохранять его, а также отображать описание наиболее популярных.
2. Начинаем работу
Создадим новый проект в XCode. В качестве устройства выберем iPhone и не забудем оставить галочку возле поля “use Core Data” в окне мастера создания проектов.
Теперь вернемся на портал и рассмотрим структуру объекта
Character
:Character object
Что из этого нам может понадобиться? Пожалуй, ограничимся идентификатором, именем, картинкой и описанием. Давайте перейдем к нашему *.xcdatamodeld файлу в XCode и создадим сущность Character {
id (int, optional): The unique ID of the character resource.,
name (string, optional): The name of the character.,
description (string, optional): A short bio or description of the character.,
modified (Date, optional): The date the resource was most recently modified.,
resourceURI (string, optional): The canonical URL identifier for this resource.,
urls (Array[Url], optional): A set of public web site URLs for the resource.,
thumbnail (Image, optional): The representative image for this character.,
comics (ComicList, optional): A resource list containing comics which feature this character.,
stories (StoryList, optional): A resource list of stories in which this character appears.,
events (EventList, optional): A resource list of events in which this character appears.,
series (SeriesList, optional): A resource list of series in which this character appears.
}
Character
, которая логически будет соответствовать (хоть и частично) нашему удаленному объекту.Я специально создал два идентификатора: первый,
charID
, будет служить для хранения “родного Marvel’овского” id
на будущее, второй же, innerID
, будет необходим для локального использования. Атрибуты charDescription
и name соотвествуют удаленным параметрам description и name соответственно.Обратите внимание, что я также создал два атрибута
thumbnailImageData
и thumbnailURLString
, хотя они не соответствуют ни одному параметру оригинальной структуры. Это вызвано тем, что в JSON-ответе thumbnail
типа Image
и в реальности соответствует словарю. Вот пример объекта thumbnail
из реального ответа:"thumbnail": {
"path": "http://i.annihil.us/u/prod/marvel/i/mg/8/c0/4ce5a0e31f109",
"extension": "jpg"
}
В дальнейшем будет показано, как мы будем работать с этим.
Теперь для правильной работы с сущностями Core Data необходимо также создать Objective-C класс, который будет ее представлять. Создадим класс
Character
, который будет наследоавться от NSManagedObject
. Вот его объявление:@interface Character : NSManagedObject {
NSDictionary *_thumbnailDictionary;
}
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *charID;
@property (nonatomic, retain) NSNumber *innerID;
@property (nonatomic, retain) NSString *charDescription;
@property (nonatomic, retain) NSData *thumbnailImageData;
@property (nonatomic, retain) NSString *thumbnailURLString;
@property NSDictionary *thumbnailDictionary;
// Получает число всех героев из базы
+ (NSInteger)allCharsCountWithContext:(NSManagedObjectContext *)managedObjectContext;
// Возвращает героя по его innerID.
+ (Character *)charWithManagedObjectContext:(NSManagedObjectContext *)context andInnerID:(NSInteger)charInnerID;
@end
Здесь, помимо очевидных соотвествий, появилось свойство
thumbnailDictionary
, которое я добавил для более удобной работы с объектом thumbnail, о котором я писал немного выше. Также я добавил два вспомогательных метода класса, чтобы не создавать в проекте дополнительных классов.3. Модель для работы с RestKit
Подключим к нашему проекту RestKit (далее — RK). Как это сделать, подробно расписано здесь (или здесь, если Вы — любитель CocoaPods).
Следующим шагом станет создание класса-обертки
GDMarvelRKObjectManager
(наследник NSObject
), который будет работать с RK, в частности с такими классами, как RKObjectManager
и RKManagedObjectStore
. Этот класс можно и не создавать, однако мы пойдем на это, чтобы немного разгрузить код в нашем будущем главном вью-контроллере.Немного о классах RK.
RKManagedObjectStore
инкапсулирует всю работу с Core Data, так что в дальнейшем не будет необходимости работать с NSManagedObjectContext
или NSManagedObjectModel
напрямую. RKObjectManager
предоставляет централизованный интерфейс для отправки запросов и получения ответов, используя маппинг (соответствие) объектов. Например, нужные значения, полученные в JSON-ответе, при успешном маппинге будут автоматически присваиваться всем свойствам нашего объекта. Не этого ли мы так хотели в начале статьи?Не забудьте включить заголовок RK
#import <RestKit/RestKit.h>
в ваш *.h файл.Наш класс-обертка не будет иметь свойств, но будет иметь две переменных экземпляра:
@implementation GDMarvelRKObjectManager {
RKObjectManager *objectManager;
RKManagedObjectStore *managedObjectStore;
}
Давайте рассмотрим, что нам необходимо настроить, чтобы все работало, как надо.
Для начала в
- (id)init
методе добавим инициализацию нужных объектов RK:// Инициализация AFNetworking HTTPClient
NSURL *baseURL = [NSURL URLWithString:@"http://gateway.marvel.com/"];
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:baseURL];
//Инициализация RKObjectManager
objectManager = [[RKObjectManager alloc] initWithHTTPClient:client];
Теперь наши запросы будут отправляться. Что насчет работы с Core Data? Давайте создадим метод, который бы конфигурировал объект типа RKManagedObjectStore.
- (void)configureWithManagedObjectModel:(NSManagedObjectModel *)managedObjectModel {
if (!managedObjectModel)
return;
managedObjectStore = [[RKManagedObjectStore alloc] initWithManagedObjectModel:managedObjectModel];
NSError *error;
if (!RKEnsureDirectoryExistsAtPath(RKApplicationDataDirectory(), &error))
RKLogError(@"Failed to create Application Data Directory at path '%@': %@", RKApplicationDataDirectory(), error);
NSString *path = [RKApplicationDataDirectory() stringByAppendingPathComponent:@"RKMarvel.sqlite"];
if (![managedObjectStore addSQLitePersistentStoreAtPath:path
fromSeedDatabaseAtPath:nil
withConfiguration:nil options:nil error:&error])
RKLogError(@"Failed adding persistent store at path '%@': %@", path, error);
[managedObjectStore createManagedObjectContexts];
objectManager.managedObjectStore = managedObjectStore;
}
Последняя строка очень важна. Она связывает между собой два наших главных RK-объекта:
objectManager
и managedObjectStore
.Итак, наша дальнейшая задача — создать в нашем классе
GDMarvelRKObjectManager
интерфейс для двух главных действий: добавление маппинга (соответствия) между сущностью Core Data и удаленным объектом, а также получение этих объектов от удаленного сервера.Первая задача реализуется в следующем методе:
- (void)addMappingForEntityForName:(NSString *)entityName
andAttributeMappingsFromDictionary:(NSDictionary *)attributeMappings
andIdentificationAttributes:(NSArray *)ids
andPathPattern:(NSString *)pathPattern {
if (!managedObjectStore)
return;
RKEntityMapping *objectMapping = [RKEntityMapping mappingForEntityForName:entityName
inManagedObjectStore:managedObjectStore];
// Указываем, какие атрибуты должны мапиться.
[objectMapping addAttributeMappingsFromDictionary:attributeMappings];
// Указываем, какие атрибуты являются идентификаторами. Важно для того, чтобы не было дубликатов в локальной базе.
objectMapping.identificationAttributes = ids;
// Создаем дескриптор ответа, ориентируясь на формат ответов нашего сервера и добавляем его в менеджер.
RKResponseDescriptor *characterResponseDescriptor =
[RKResponseDescriptor responseDescriptorWithMapping:objectMapping
method:RKRequestMethodGET
pathPattern:[NSString stringWithFormat:@"%@%@", MARVEL_API_PATH_PATTERN, pathPattern]
keyPath:@"data.results"
statusCodes:[NSIndexSet indexSetWithIndex:200]];
[objectManager addResponseDescriptor:characterResponseDescriptor];
}
Тут нас интересуют несколько параметров у метода
responseDescriptorWithMapping:...
Во-первых — параметр pathPattern
. Получается путем конкатенации макроса MARVEL_API_PATH_PATTERN
(со значением @"v1/public/"
) и входного параметра pathPattern
, который в нашем примере будет равен @"characters"
. Если же мы захотим получить не список персонажей, а, допустим, список комиксов, то передавать мы будем строку @”comics”
, которая уже в теле метода вновь соединится с @"v1/public/"
.Второе неочевидное значение — это параметр
@"data.results"
для параметра keyPath
. Откуда оно взялось? Все очень просто: Marvel оборачивают все свои ответы в однотипную обертку, и все станет на свои места, когда мы посмотрим на ее структуру:Characters wrapper
{
"code": "int",
"status": "string",
"copyright": "string",
"attributionText": "string",
"attributionHTML": "string",
"data": {
"offset": "int",
"limit": "int",
"total": "int",
"count": "int",
"results": [
{
"id": "int",
"name": "string",
"description": "string",
"modified": "Date",
"resourceURI": "string",
"urls": [
{
"type": "string",
"url": "string"
}
],
"thumbnail": {
"path": "string",
"extension": "string"
},
"comics": {
"available": "int",
"returned": "int",
"collectionURI": "string",
"items": [
{
"resourceURI": "string",
"name": "string"
}
]
},
"stories": {
"available": "int",
"returned": "int",
"collectionURI": "string",
"items": [
{
"resourceURI": "string",
"name": "string",
"type": "string"
}
]
},
"events": {
"available": "int",
"returned": "int",
"collectionURI": "string",
"items": [
{
"resourceURI": "string",
"name": "string"
}
]
},
"series": {
"available": "int",
"returned": "int",
"collectionURI": "string",
"items": [
{
"resourceURI": "string",
"name": "string"
}
]
}
}
]
},
"etag": "string"
}
Теперь понятно, что прежде чем достучаться до собственно списка героев, RK придется пройтись по словарям на несколько уровней вниз, чтобы добраться до нужной структуры. Значение
@"data.results"
как раз указывает тот путь, по которому надо “спуститься”.Вторым методом нашего класса для работы с внутренним объектом RK будет
getMarvelObjectsAtPath
, который по сути проксирует обращение к getObjectsAtPath
объекта типа RKObjectManager
. Название у метода “говорящее” — вы ждете от него загрузки удаленных объектов. Так как Marvel требуют, чтобы с каждым запросом им отправлялся hash, timestamp и открытый ключ, удобно инкапсулировать генерацию этих параметров в наш getMarvelObjectsAtPath
. Вот он:- (void)getMarvelObjectsAtPath:(NSString *)path
parameters:(NSDictionary *)params
success:(void (^)(RKObjectRequestOperation *operation, RKMappingResult *mappingResult))success
failure:(void (^)(RKObjectRequestOperation *operation, NSError *error))failure {
// Подготовка нужных параметров
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyyMMddHHmmss"];
NSString *timeStampString = [formatter stringFromDate:[NSDate date]];
NSString *hash = [[[NSString stringWithFormat:@"%@%@%@", timeStampString, MARVEL_PRIVATE_KEY, MARVEL_PUBLIC_KEY] MD5String] lowercaseString];
NSMutableDictionary *queryParams = [NSMutableDictionary dictionaryWithDictionary:@{@"apikey" : MARVEL_PUBLIC_KEY,
@"ts" : timeStampString,
@"hash" : hash}];
if (params)
[queryParams addEntriesFromDictionary:params];
// Непосредственный вызов метода у объекта objectManager с вновь собранными параметрами
[objectManager getObjectsAtPath:[NSString stringWithFormat:@"%@%@", MARVEL_API_PATH_PATTERN, path]
parameters:queryParams
success:success
failure:failure];
}
Обратите внимание, что в коде используется метод из нестандартной категории над
NSString
— MD5String
. Как сгенерировать MD5-троку от строки, поищите в интернете.У нашего класса еще будет простой метод
- (NSManagedObjectContext *)managedObjectContext
, который будет возвращать главный контекст managedObjectStore
. Также этот класс будет синглтоном (Singleton) с методом + (GDMarvelRKObjectManager *)manager
для доступа к экземпляру.4. Главный ViewController
Для начала создадим базовый вью-контроллер
GDBaseViewController
, в котором мы просто встроим поддержку анимации ожидания ответа от сервера с единственным новым методом - (void)animateActivityIndicator:(BOOL)animate
. В методе viewDidLoad
создадим этот индикатор типа UIActivityIndicatorView
, присвоим полученное значение переменной экземпляра UIActivityIndicatorView *activityIndicator
и добавим его на self.view
.В самом методе включения/выключения анимации будет следующий код:
animateActivityIndicator: code
- (void)animateActivityIndicator:(BOOL)animate {
activityIndicator.hidden = !animate;
if (animate) {
[self.view bringSubviewToFront:activityIndicator];
[activityIndicator startAnimating];
}
else
[activityIndicator stopAnimating];
}
Теперь, когда мы будем вызывать этот метод со значением
YES
для единственного параметра, наш вью-контроллер будет выглядеть вот так:Далее создадим вью-контроллер
GDMainViewController
унаследованный от этого класса. Вот его объявление:@interface GDMainViewController : GDBaseViewController <UITableViewDataSource, UITableViewDelegate, UIAlertViewDelegate> {
UITableView *table;
NSInteger numberOfCharacters;
AllAroundPullView *bottomPullView;
BOOL noRequestsMade;
}
@end
В этом вью-контроллере мы будем отображать данные из БД. Для этого будем использовать экземпляр
UITableView
, на котором в каждой ячейке отображаются картинка и имя каждого из персонажей. Но их надо еще загрузить, так как изначально локальная база пуста. После всего инициализирующего процесса, присущего созданию экземпляра UITableView
в методе - (void)viewDidLoad
, мы сначала привяжем нашу CoreData-модель к RKManagedObjectStore
, используя наш класс-обертку GDMarvelRKObjectManager
:NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Marvel" withExtension:@"momd"];
[[GDMarvelRKObjectManager manager] configureWithManagedObjectModel:[[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]];
// Затем добавим маппинг для нашего объекта типа Character:
[[GDMarvelRKObjectManager manager] addMappingForEntityForName:@"Character"
andAttributeMappingsFromDictionary:@{
@"name" : @"name",
@"id" : @"charID",
@"thumbnail" : @"thumbnailDictionary",
@"description" : @"charDescription"
}
andIdentificationAttributes:@[@"charID"]
andPathPattern:MARVEL_API_CHARACTERS_PATH_PATTERN];
Как видите, в качестве параметра
andAttributeMappingsFromDictionary:
передается словарь, состоящий из соответствий между названиями JSON-ключей удаленного объекта и свойств созданного нами класса. В качестве параметра andPathPattern:
передается строка @"characters"
(макрос MARVEL_API_CHARACTERS_PATH_PATTERN
) — имя удаленного JSON-объекта.После того, как мы добавили маппинг, вызовем метод
[self loadCharacters]
.Рассмотрим подробно, что он делает:
- (void)loadCharacters {
numberOfCharacters = [Character allCharsCountWithContext:[[GDMarvelRKObjectManager manager] managedObjectContext]];
if (noRequestsMade && numberOfCharacters > 0) {
noRequestsMade = NO;
return;
}
[self animateActivityIndicator:YES];
noRequestsMade = NO;
[[GDMarvelRKObjectManager manager] getMarvelObjectsAtPath:MARVEL_API_CHARACTERS_PATH_PATTERN
parameters:@{@"offset" : @(numberOfCharacters)}
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[self animateActivityIndicator:NO];
NSInteger newInnerID = numberOfCharacters;
for (Character *curCharacter in mappingResult.array) {
if ([curCharacter isKindOfClass:[Character class]]) {
curCharacter.innerID = @(newInnerID);
newInnerID++;
//Сохраняем каждого персонажа по одному (а не всех вместе после цикла), чтобы предотвратить потери, если программа аварийно завершится в середине цикла
[self saveToStore];
}
}
numberOfCharacters = newInnerID;
[table reloadData];
bottomPullView.hidden = NO;
[bottomPullView finishedLoading];
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {
[bottomPullView finishedLoading];
[[[UIAlertView alloc] initWithTitle:@"Marvel API Error" message:operation.error.localizedDescription delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Retry", nil] show];
}];
}
Сначала мы получаем общее количество персонажей из локальной базы, это значение будет соответствовать количеству ячеек в главной таблице. При первом запуске приложения оно, естественно, будет равняться нулю. Это же значение мы будем использовать в качестве передаваемого параметра offset при обращении к серверу. Таким образом на каждый следующий запрос сервер Marvel будет возвращать только новые объекты героев (по умолчанию герои возвращаются пачками по 20 штук в каждой).
Далее мы производим тот самый главный запрос, используя наш метод-обертку
getMarvelObjectsAtPath
:У этого метода два важных для нас сейчас параметра — это success: и failure:, которые являются блоками, описывающими поведение при успешном и не успешном результатах выполнения запроса соответственно. Итак, при успешном получении массива персонажей, мы генерируем для каждого из них
innerID
, сохраняем их в локальную базу и изменяем значение общего количества героев. После чего обновляем отображение нашей таблицы. Самая главная магия здесь заключается в том, что на этом этапе полученные объекты уже автоматически сохранились в нашем CoreData-хранилище — RK сделал это за нас. (Стоит отметить, что это касается только тех полей/свойств объекта, для которого заданы маппинг-соответсвия. Так, в коде выше зменение параметра innerID
приходится соханять отдельно, вызвав [self saveToStore]
).В случае возникновении какой-то ошибки мы просто выводим ее пользователю и не обновляем таблицу.
В коде используется метод сохранения в хранилище:
- (void)saveToStore {
NSError *saveError;
if (![[[GDMarvelRKObjectManager manager] managedObjectContext] saveToPersistentStore:&saveError])
XLog(@"%@", [saveError localizedDescription]);
}
Также вы заметите обращение к переменной экземпляра
bottomPullView
. Эта переменная хранит объект типа AllAroundPullView
(cтянуть с GitHub) — полезный контрол, помогающий реализовать поведение Pull-To-Resfresh со всех сторон вашего UIScrollView
. Мы будем подгружать каждую очередную порцию наших персонажей, дойдя до нижнего края таблицы и потянув ее вверх.Ранее в
- (void)viewDidLoad
этот контрол был инициализирован и использован следующим образом:bottomPullView = [[AllAroundPullView alloc] initWithScrollView:table position:AllAroundPullViewPositionBottom action:^(AllAroundPullView *view){
[self loadCharacters];
}];
bottomPullView.hidden = YES;
[table addSubview:bottomPullView];
Как видите, в теле блока, передаваемого в качестве параметра action: мы поместили все тот же метод подгрузки новых героев
loadCharacters
.Что ж, запустим приложение в эмуляторе и дождемся первого успешного ответа. Если все прошло правильно, и логгер RK вывел что-то наподобие
I restkit.network:RKObjectRequestOperation.m:220 GET 'http://your-url.here' (200 OK / 20 objects)
, значит все хорошо, и можно проверить, сохранились ли наши объекты в базу.Для этого зайдем в папку эмулятора, найдем там наше приложение и папку Documents. Там должна находиться база
RKMarvel.sqlite
(именно такое имя мы указали в качестве параметра при вызове метода addSQLitePersistentStoreAtPath:
ранее). Откроем эту базу в SQLite-редакторе и удостоверимся в том, что наши персонажи сохранены:Ура! У некоторых героев даже есть небольшое описание. Самое время перейти к отображению всего этого «добра».
5. Сохранение картинок и отображение.
Я знаю, что нетерпеливый читатель уже давно хочет посмотреть на изображения любимых персонажей. Для этого нам необходимо настроить внешний вид нашей таблицы. Не будем вдаваться в технические подробности создания и настройки объектов типа
UITableView
(автор предполагает, что это читателю уже известно), а сразу перейдем к методу делегата таблицы, который создает ячейки:tableView:cellForRowAtIndexPath: code
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
NSString *reusableIdentifier = [NSString stringWithFormat:@"%d", row % 2];
UITableViewCell *cell = [table dequeueReusableCellWithIdentifier:reusableIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reusableIdentifier];
cell.autoresizingMask = UIViewAutoresizingFlexibleWidth;
}
[[cell.contentView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
if (numberOfCharacters > row) {
Character *curCharacter = [Character charWithManagedObjectContext:
[[GDMarvelRKObjectManager manager] managedObjectContext]
andInnerID:row];
if (curCharacter) {
BOOL charHasDescription = ![curCharacter.charDescription isEqualToString:@""];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(70, 0, CGRectGetWidth(cell.contentView.frame) - 70 - (charHasDescription ? 60 : 0), 60)];
label.backgroundColor = [UIColor clearColor];
label.text = curCharacter.name;
label.autoresizingMask = UIViewAutoresizingFlexibleWidth;
[cell.contentView addSubview:label];
GDCellThumbnailView *thumbnail = [GDCellThumbnailView thumbnail];
if (curCharacter.thumbnailImageData)
[thumbnail setImage:[UIImage imageWithData:curCharacter.thumbnailImageData]];
else
[self loadThumbnail:thumbnail fromURLString:curCharacter.thumbnailURLString forCharacter:curCharacter];
[cell.contentView addSubview:thumbnail];
cell.accessoryType = charHasDescription ? UITableViewCellAccessoryDetailButton : UITableViewCellSelectionStyleNone;
cell.selectionStyle = charHasDescription ? UITableViewCellSelectionStyleGray : UITableViewCellSelectionStyleNone;
}
}
return cell;
}
После создания очередной ячейки мы достаем нужного героя из базы и отображаем его имя, также мы проверяем, присутствует ли развернутая информация о нем, и помещаем на ячейку кнопку, по нажатию на которую эту информацию потом отобразим. Ну и самое главное — изображение персонажа. Я создал для этого специальный класс
GDCellThumbnailView
, экземпляры которого я и помещаю на ячейку. Он не делает ничего особенного, просто у него есть возможность показывать нам “крутящийся цветочек” ожидания, пока thumbnail не загрузился.При пустой реализации метода
loadThumbnail:fromURLString:forCharacter:
наш главный вью-контроллер теперь будет выглядеть так:Давайте реализуем метод загрузки картинки героя. Так как RK уже включает в себя фреймворк
AFNetworking
, будем использовать его для отправки асинхронного запроса к серверам Marvel для загрузки картинок:- (void)loadThumbnail:(GDCellThumbnailView *)view fromURLString:(NSString *)urlString forCharacter:(Character *)character {
XLog(@"Loading thumbnail for %@", character.name);
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
character.thumbnailImageData = responseObject;
[self saveToStore];
[view setImage:[UIImage imageWithData:responseObject]];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
XLog(@"%@", [error localizedDescription]);
}];
[operation start];
}
Вот и все. Запустим наше приложение еще раз. Уже хороший результат.
Теперь будет трудно остановиться, и я с вашего позволения использую удобный Pull-To-Refresh контрол для загрузки большего количества персонажей. Заодно проверим, как теперь выглядит наша база.
Теперь и картинки, и информация о героях (естественно только тех, которых мы успели загрузить) будут хранится локально вне зависимости от того, есть у нас соединение с Интернет или нет.
6. Заключение.
RestKit прекрасно справился с поставленной задачей: запросы отправляются, ответы получаются, объекты сохраняются автоматически. Не всем может понравиться сам принцип загрузки и отображения, предоставленный в этой статье: возможно, что разумнее было бы сразу выкачать всю базу и работать с ней полностью локально. Автор считает, что для ознакомления с базовыми возможностями RK такой функциональности вполне достаточно. Исходный код всего проекта (вместе с недостающей в этой статье частью с отображением информации о конкретном персонаже) можно скачать на GitHub. Ваши пожелания и замечания приветствуются в качестве комментариев к статье, а также пул-реквестов на GitHub.
Напоследок хочется порадовать еще одним изображением — на сей раз это скриншот второго вью-контроллера, который открывается по нажатию на кнопочку “info” возле имени героя в главном вью-контроллере. Уж очень долго я прокручивал свою таблицу, чтоб наконец загрузить его: