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

Веб-сервисы, в частности использующие REST-архитектуру, уже плотно вошли в нашу жизнь. Разрабатывая клиентское приложение под iOS, часто так или иначе приходится загружать данные с сервера и хранить/отображать их локально. При этом хочется делать это легко и непринужденно, не прибегая к изобретению собственных «велосипедов».Последняя версия известного Objective-C фреймворка RestKit для iOS и OSX значительно упрощает работу с RESTful API. Несомненно, одной из его самых ценных фич является возможность автоматического сохранения объектов в локальную БД, используя CoreData. Давайте вместе проделаем путь от получения данных от сервера до сохранения и отображения их на нашем iOS-устройстве. А чтобы нам не было скучно, в качестве примера будем работать с API всемирно известной компании по производству комиксов Marvel.

Статья представляет из себя некое подобие туториала. Предполагается, что читатель уже знаком с базовыми концепциями разработки на языке Objective-C, использованием iOS SDK, Core Data и такого понятия как блоки.

5ee92575ca0c780d985b34e2d601c379.jpg

1. Получаем ключи Marvel и формулируем задачу Для начала давайте зарегистрируемся как разработчик на сайте Marvel.После тривиальной регистрации переходим на вкладку Account и копируем наши открытый и закрытый ключи.91d09bfc67503141f8423c184292e360.pngПосле этого перейдем на вкладку 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, которая логически будет соответствовать (хоть и частично) нашему удаленному объекту.5e3b9814aecc0f29265859d0bf1062bb.pngЯ специально создал два идентификатора: первый, 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 в ваш *.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 для единственного параметра, наш вью-контроллер будет выглядеть вот так: f8f13dea4106e51d7002993fd746554c.pngДалее создадим вью-контроллер GDMainViewController унаследованный от этого класса. Вот его объявление:

@interface GDMainViewController: GDBaseViewController { 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-редакторе и удостоверимся в том, что наши персонажи сохранены: 154750da1aab1c647c38d08e0d1f1ebc.pngУра! У некоторых героев даже есть небольшое описание. Самое время перейти к отображению всего этого «добра».

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: наш главный вью-контроллер теперь будет выглядеть так: 91df289767f79735b7d8fd55dc681258.png

Давайте реализуем метод загрузки картинки героя. Так как 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]; } Вот и все. Запустим наше приложение еще раз. Уже хороший результат.6f4ad8b73ec894b492e8970e257749b2.pngТеперь будет трудно остановиться, и я с вашего позволения использую удобный Pull-To-Refresh контрол для загрузки большего количества персонажей. Заодно проверим, как теперь выглядит наша база.74f30534950a437ef710a9506ec04e09.png76845173b211b5fc98dc6cd6672ac65f.pngТеперь и картинки, и информация о героях (естественно только тех, которых мы успели загрузить) будут хранится локально вне зависимости от того, есть у нас соединение с Интернет или нет.

6. Заключение. RestKit прекрасно справился с поставленной задачей: запросы отправляются, ответы получаются, объекты сохраняются автоматически. Не всем может понравиться сам принцип загрузки и отображения, предоставленный в этой статье: возможно, что разумнее было бы сразу выкачать всю базу и работать с ней полностью локально. Автор считает, что для ознакомления с базовыми возможностями RK такой функциональности вполне достаточно. Исходный код всего проекта (вместе с недостающей в этой статье частью с отображением информации о конкретном персонаже) можно скачать на GitHub. Ваши пожелания и замечания приветствуются в качестве комментариев к статье, а также пул-реквестов на GitHub.Напоследок хочется порадовать еще одним изображением — на сей раз это скриншот второго вью-контроллера, который открывается по нажатию на кнопочку «info» возле имени героя в главном вью-контроллере. Уж очень долго я прокручивал свою таблицу, чтоб наконец загрузить его: 87bf55131e3ca0636ef844fbad659a6b.png

© Habrahabr.ru