Последняя версия известного 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 { 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 оборачивают все свои ответы в однотипную обертку, и все станет на свои места, когда мы посмотрим на ее структуру:
{ "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
.
В самом методе включения/выключения анимации будет следующий код:
- (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
(автор предполагает, что это читателю уже известно), а сразу перейдем к методу делегата таблицы, который создает ячейки:
- (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/
Добавить комментарий