Pull to refresh

Используем RestKit 0.22.x для просмотра героев Marvel

Reading time 17 min
Views 9.9K
Веб-сервисы, в частности использующие 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” возле имени героя в главном вью-контроллере. Уж очень долго я прокручивал свою таблицу, чтоб наконец загрузить его:
Tags:
Hubs:
+3
Comments 2
Comments Comments 2

Articles