...

понедельник, 28 апреля 2014 г.

[Из песочницы] Используем RestKit 0.22.x для просмотра героев Marvel

Веб-сервисы, в частности использующие REST-архитектуру, уже плотно вошли в нашу жизнь. Разрабатывая клиентское приложение под iOS, часто так или иначе приходится загружать данные с сервера и хранить/отображать их локально. При этом хочется делать это легко и непринужденно, не прибегая к изобретению собственных “велосипедов”.

Последняя версия известного 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


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



Что из этого нам может понадобиться? Пожалуй, ограничимся идентификатором, именем, картинкой и описанием. Давайте перейдем к нашему *.xcdatamodeld файлу в XCode и создадим сущность Character, которая логически будет соответствовать (хоть и частично) нашему удаленному объекту.



Я специально создал два идентификатора: первый, charID, будет служить для хранения “родного Marvel’овского” id на будущее, второй же, innerID, будет необходим для локального использования. Атрибуты charDescription и name соотвествуют удаленным параметрам description и name соответственно.

Обратите внимание, что я также создал два атрибута thumbnailImageData и thumbnailURLString, хотя они не соответствуют ни одному параметру оригинальной структуры. Это вызвано тем, что в JSON-ответе thumbnail типа Image и в реальности соответствует словарю. Вот пример объекта thumbnail из реального ответа:



"thumbnail": {
"path": "http://ift.tt/1hFI4Eu",
"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://ift.tt/1k1EbXL"];
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];
}




Обратите внимание, что в коде используется метод из нестандартной категории над NSStringMD5String. Как сгенерировать 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” возле имени героя в главном вью-контроллере. Уж очень долго я прокручивал свою таблицу, чтоб наконец загрузить его:


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий