Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

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

Anna | 2.07.2014 | нет комментариев

Веб-сервисы, в частности использующие 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, которая логически будет соответствовать (хоть и Отчасти) нашему удаленному объекту.


Я намеренно сотворил два идентификатора: 1-й, charID, будет служить для хранения “родного Marvel’овского”id на грядущее, 2-й же, 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 интерфейс для 2-х основных действий: добавление маппинга (соответствия) между сущностью 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 для исключительного параметра, наш вью-контроллер будет выглядеть вот так:

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

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB