Используем 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://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]; } 

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

ссылка на оригинал статьи http://habrahabr.ru/post/220885/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *