Simplificando o uso da API RESTful e a persistência de dados no iOS com Mantle e Realm

Publicados: 2022-03-11

Todo desenvolvedor iOS está familiarizado com o Core Data, um framework de persistência e gráfico de objetos da Apple. Além de persistir os dados localmente, a estrutura vem com uma série de recursos avançados, como rastreamento de alterações de objetos e desfazer. Esses recursos, embora úteis em muitos casos, não são gratuitos. Requer muito código clichê e a estrutura como um todo tem uma curva de aprendizado íngreme.

Em 2014, o Realm, um banco de dados móvel, foi lançado e conquistou o mundo do desenvolvimento. Se tudo o que precisamos é persistir os dados localmente, o Realm é uma boa alternativa. Afinal, nem todos os casos de uso exigem os recursos avançados do Core Data. O Realm é extremamente fácil de usar e, ao contrário do Core Data, requer muito pouco código clichê. Também é seguro para threads e é considerado mais rápido que o framework de persistência da Apple.

Na maioria dos aplicativos móveis modernos, a persistência de dados resolve metade do problema. Muitas vezes, precisamos buscar dados de um serviço remoto, geralmente por meio de uma API RESTful. É aqui que o Mantle entra em cena. É uma estrutura de modelo de código aberto para Cocoa e Cocoa Touch. O Mantle simplifica significativamente a gravação de modelos de dados para interação com APIs que usam JSON como formato de troca de dados.

Reino e Manto para iOS

Neste artigo, construiremos um aplicativo iOS que busca uma lista de artigos junto com links para eles da API de pesquisa de artigos do New York Times v2. A lista será buscada usando uma solicitação HTTP GET padrão, com modelos de solicitação e resposta criados usando Mantle. Veremos como é fácil com o Mantle lidar com transformações de valor (por exemplo, de NSDate para string). Depois que os dados forem buscados, persistiremos localmente usando o Realm. Tudo isso com código clichê mínimo.

API RESTful - Introdução

Vamos começar criando um novo projeto Xcode “Master-Detail Application” para iOS chamado “RealmMantleTutorial”. Estaremos adicionando frameworks a ele usando CocoaPods. O podfile deve se parecer com o seguinte:

 pod 'Mantle' pod 'Realm' pod 'AFNetworking'

Depois que os pods estiverem instalados, podemos abrir o recém-criado espaço de trabalho MantleRealmTutorial . Como você notou, o famoso framework AFNetworking também foi instalado. Nós o usaremos para realizar solicitações à API.

Conforme mencionado na introdução, o New York Times oferece uma excelente API de pesquisa de artigos. Para usá-lo, é necessário se inscrever para obter uma chave de acesso à API. Isso pode ser feito em http://developer.nytimes.com. Com a chave de API em mãos, estamos prontos para começar a codificar.

Antes de nos aprofundarmos na criação de modelos de dados do Mantle, precisamos colocar nossa camada de rede em funcionamento. Vamos criar um novo grupo no Xcode e chamá-lo de Network. Neste grupo estaremos criando duas classes. Vamos chamar o primeiro SessionManager e ter certeza de que ele é derivado de AFHTTPSessionManager que é uma classe de gerenciador de sessão do AFNetworking , o maravilhoso framework de rede. Nossa classe SessionManager será um objeto singleton que usaremos para realizar solicitações de obtenção para a API. Depois que a classe for criada, copie o código abaixo para os arquivos de interface e implementação, respectivamente.

 #import "AFHTTPSessionManager.h" @interface SessionManager : AFHTTPSessionManager + (id)sharedManager; @end
 #import "SessionManager.h" static NSString *const kBaseURL = @"http://api.nytimes.com"; @implementation SessionManager - (id)init { self = [super initWithBaseURL:[NSURL URLWithString:kBaseURL]]; if(!self) return nil; self.responseSerializer = [AFJSONResponseSerializer serializer]; self.requestSerializer = [AFJSONRequestSerializer serializer]; return self; } + (id)sharedManager { static SessionManager *_sessionManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sessionManager = [[self alloc] init]; }); return _sessionManager; } @end

O gerenciador de sessão é inicializado com a URL base definida na variável estática kBaseURL . Ele também usará serializadores de solicitação e resposta JSON.

Agora a segunda classe que vamos criar no grupo Network se chamará APIManager . Deve ser derivado de nossa classe SessionManager recém-criada. Uma vez criados os modelos de dados necessários, adicionaremos um método ao ApiManager que será usado para solicitar uma lista de artigos da API.

Visão geral da API de pesquisa de artigos do New York Times

A documentação oficial para esta excelente API está disponível em http://developer.nytimes.com/…/article_search_api_v2. O que vamos fazer é usar o seguinte endpoint:

 http://api.nytimes.com/svc/search/v2/articlesearch

… para buscar artigos encontrados usando um termo de consulta de pesquisa de nossa escolha delimitado por um intervalo de datas. Por exemplo, o que podemos fazer é pedir à API que retorne uma lista de todos os artigos que apareceram no New York Times que tiveram alguma coisa a ver com basquete nos primeiros sete dias de julho de 2015. De acordo com a documentação da API, fazer isso precisamos definir os seguintes parâmetros na solicitação get para esse endpoint:

Parâmetro Valor
q "basquetebol"
data_inicial “20150701”
data final “20150707”

A resposta da API é bastante complexa. Abaixo está a resposta para uma solicitação com os parâmetros acima limitados a apenas um artigo (um item na matriz docs) com vários campos omitidos para maior clareza.

 { "response": { "docs": [ { "web_url": "http://www.nytimes.com/2015/07/04/sports/basketball/robin-lopez-and-knicks-are-close-to-a-deal.html", "lead_paragraph": "Lopez, a 7-foot center, joined Arron Afflalo, a 6-foot-5 guard, as the Knicks' key acquisitions in free agency. He is expected to solidify the Knicks' interior defense.", "abstract": null, "print_page": "1", "source": "The New York Times", "pub_date": "2015-07-04T00:00:00Z", "document_type": "article", "news_desk": "Sports", "section_name": "Sports", "subsection_name": "Pro Basketball", "type_of_material": "News", "_id": "5596e7ac38f0d84c0655cb28", "word_count": "879" } ] }, "status": "OK", "copyright": "Copyright (c) 2013 The New York Times Company. All Rights Reserved." }

O que basicamente recebemos em resposta são três campos. O primeiro chamado response contém o array docs , que por sua vez contém itens que representam artigos. Os outros dois campos são status e copyright . Agora que sabemos como a API funciona, é hora de criar modelos de dados usando o Mantle.

Introdução ao Manto

Como mencionado anteriormente, o Mantle é uma estrutura de código aberto que simplifica significativamente a gravação de modelos de dados. Vamos começar criando um modelo de solicitação de lista de artigos. Vamos chamar essa classe de ArticleListRequestModel e garantir que ela seja derivada de MTLModel , que é uma classe da qual todos os modelos Mantle devem ser derivados. Além disso, vamos torná-lo compatível com o protocolo MTLJSONSerializing . Nosso modelo de solicitação deve ter três propriedades de tipos adequados: query, articleFromDate e articleToDate . Apenas para garantir que nosso projeto esteja bem organizado, sugiro que esta classe seja colocada no grupo Modelos .

Mantle simplifica a gravação de modelos de dados, reduz o código clichê.
Tweet

Veja como deve ser a aparência do arquivo de interface de ArticleListRequestModel :

 #import "MTLModel.h" #import "Mantle.h" @interface ArticleListRequestModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSString *query; @property (nonatomic, copy) NSDate *articlesFromDate; @property (nonatomic, copy) NSDate *articlesToDate; @end

Agora, se procurarmos os documentos para nosso endpoint de pesquisa de artigos ou dermos uma olhada na tabela com os parâmetros de solicitação acima, perceberemos que os nomes das variáveis ​​na solicitação de API diferem daqueles em nosso modelo de solicitação. Mantle lida com isso de forma eficiente usando o método:

 + (NSDictionary *)JSONKeyPathsByPropertyKey.

Veja como esse método deve ser implementado na implementação do nosso modelo de solicitação:

 #import "ArticleListRequestModel.h" @implementation ArticleListRequestModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"query": @"q", @"articlesFromDate": @"begin_date", @"articlesToDate": @"end_date" }; } @end

A implementação desse método especifica como as propriedades do modelo são mapeadas em suas representações JSON. Uma vez que o método JSONKeyPathsByPropertyKey tenha sido implementado, podemos obter uma representação de dicionário JSON do modelo com o método de classe +[MTLJSONAdapter JSONArrayForModels:] .

Uma coisa que ainda resta, como sabemos da lista de parâmetros, é que os dois parâmetros de data precisam estar no formato “AAAAMMDD”. É aqui que Mantle fica muito útil. Podemos adicionar transformação de valor personalizado para qualquer propriedade implementando o método opcional +<propertyName>JSONTransformer . Ao implementá-lo, informamos ao Mantle como o valor de um campo JSON específico deve ser transformado durante a desserialização do JSON. Também podemos implementar um transformador reversível que será usado ao criar um JSON a partir do modelo. Como precisamos transformar um objeto NSDate em uma string, também faremos uso da classe NSDataFormatter . Aqui está a implementação completa da classe ArticleListRequestModel :

 #import "ArticleListRequestModel.h" @implementation ArticleListRequestModel + (NSDateFormatter *)dateFormatter { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.dateFormat = @"yyyyMMdd"; return dateFormatter; } #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"query": @"q", @"articlesFromDate": @"begin_date", @"articlesToDate": @"end_date" }; } #pragma mark - JSON Transformers + (NSValueTransformer *)articlesToDateJSONTransformer { return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter dateFromString:dateString]; } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter stringFromDate:date]; }]; } + (NSValueTransformer *)articlesFromDateJSONTransformer { return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter dateFromString:dateString]; } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter stringFromDate:date]; }]; } @end

Outra grande característica do Mantle é que todos esses modelos estão em conformidade com o protocolo NSCoding , além de implementar métodos isEqual e hash .

Como já vimos, o JSON resultante da chamada da API contém um array de objetos que representam artigos. Se quisermos modelar essa resposta usando o Mantle, teremos que criar dois modelos de dados separados. Um modelaria objetos que representam artigos (elementos do array docs ) e o outro modelaria toda a resposta JSON, exceto os elementos do array docs. Agora, não precisamos mapear cada propriedade do JSON de entrada em nossos modelos de dados. Vamos supor que estamos interessados ​​apenas em dois campos de objetos de artigo, e esses seriam lead_paragraph e web_url . A classe ArticleModel é bastante simples de implementar, como podemos ver abaixo.

 #import "MTLModel.h" #import <Mantle/Mantle.h> @interface ArticleModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSString *leadParagraph; @property (nonatomic, copy) NSString *url; @end
 #import "ArticleModel.h" @implementation ArticleModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"leadParagraph": @"lead_paragraph", @"url": @"web_url" }; } @end

Agora que o modelo de artigo foi definido, podemos concluir a definição do modelo de resposta criando um modelo para a lista de artigos. Veja como será a aparência do modelo de resposta da classe ArticleList.

 #import "MTLModel.h" #import <Mantle/Mantle.h> #import "ArticleModel.h" @interface ArticleListResponseModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSArray *articles; @property (nonatomic, copy) NSString *status; @end
 #import "ArticleListResponseModel.h" @class ArticleModel; @implementation ArticleListResponseModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"articles" : @"response.docs", @"status" : @"status" }; } #pragma mark - JSON Transformer + (NSValueTransformer *)articlesJSONTransformer { return [MTLJSONAdapter arrayTransformerWithModelClass:ArticleModel.class]; } @end

Esta classe tem apenas duas propriedades: status e artigos . Se compararmos com a resposta do endpoint, veremos que o terceiro atributo JSON copyright não será mapeado no modelo de resposta. Se observarmos o método articlesJSONTransformer , veremos que ele retorna um transformador de valor para um array contendo objetos da classe ArticleModel .

Também vale a pena notar que no método JSONKeyPathsByPropertyKey , os artigos de propriedade do modelo correspondem aos documentos da matriz que estão aninhados na resposta do atributo JSON .

Até agora, devemos ter três classes de modelo implementadas: ArticleListRequestModel, ArticleModel e ArticleListResponseModel.

Primeira solicitação de API

API Restful

Agora que implementamos todos os modelos de dados, é hora de voltar para a classe APIManager para implementar o método que usaremos para realizar requisições GET para a API. O método:

 - (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure

recebe um modelo de solicitação ArticleListRequestModel como parâmetro e retorna um ArticleListResponseModel em caso de sucesso ou um NSError caso contrário. A implementação deste método usa AFNetworking para realizar uma solicitação GET para a API. Observe que, para fazer uma solicitação de API bem-sucedida, precisamos fornecer uma chave que pode ser obtida conforme mencionado anteriormente, registrando-se em http://developer.nytimes.com.

 #import "SessionManager.h" #import "ArticleListRequestModel.h" #import "ArticleListResponseModel.h" @interface APIManager : SessionManager - (NSURLSessionDataTask *)getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure; @end
 #import "APIManager.h" #import "Mantle.h" static NSString *const kArticlesListPath = @"/svc/search/v2/articlesearch.json"; static NSString *const kApiKey = @"replace this with your own key"; @implementation APIManager - (NSURLSessionDataTask *)getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure{ NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil]; NSMutableDictionary *parametersWithKey = [[NSMutableDictionary alloc] initWithDictionary:parameters]; [parametersWithKey setObject:kApiKey forKey:@"api-key"]; return [self GET:kArticlesListPath parameters:parametersWithKey success:^(NSURLSessionDataTask *task, id responseObject) { NSDictionary *responseDictionary = (NSDictionary *)responseObject; NSError *error; ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error]; success(list); } failure:^(NSURLSessionDataTask *task, NSError *error) { failure(error); }]; }

Há duas coisas muito importantes acontecendo na implementação deste método. Primeiro vamos dar uma olhada nesta linha:

 NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil];

O que está acontecendo aqui é que, usando o método fornecido pela classe MTLJSONAdapter , obtemos uma representação NSDictionary do nosso modelo de dados. Essa representação espelha o JSON que será enviado para a API. É aqui que reside a beleza de Mantle. Tendo implementado os métodos JSONKeyPathsByPropertyKey e +<propertyName>JSONTransformer na classe ArticleListRequestModel, podemos obter a representação JSON correta do nosso modelo de dados rapidamente com apenas uma única linha de código.

Mantle também nos permite realizar transformações na outra direção também. E é exatamente isso que está acontecendo com os dados recebidos da API. O NSDictionary que recebemos é mapeado em um objeto da classe ArticleListResponseModel usando o seguinte método de classe:

 ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error];

Dados persistentes com o Realm

Agora que podemos buscar dados de uma API remota, é hora de persistir. Como mencionado na introdução, faremos isso usando o Realm. Realm é um banco de dados móvel e um substituto para Core Data e SQLite. Como veremos a seguir, é extremamente fácil de usar.

Realm, o banco de dados móvel definitivo, é um substituto perfeito para Core Data e SQLite.
Tweet

Para salvar um dado no Realm, primeiro precisamos encapsular um objeto derivado da classe RLMObject. O que precisamos fazer agora é criar uma classe de modelo que armazenará dados para artigos únicos. Veja como é fácil criar essa classe.

 #import "RLMObject.h" @interface ArticleRealm : RLMObject @property NSString *leadParagraph; @property NSString *url; @end

E pode ser basicamente isso, a implementação dessa classe pode ficar vazia. Observe que as propriedades na classe de modelo não possuem atributos como não atômico, forte ou cópia. Realm cuida deles e não precisamos nos preocupar com eles.

Como os artigos que podemos obter são modelados com o modelo Mante Article , seria conveniente inicializar objetos ArticleRealm com objetos da classe Article . Para fazer isso, adicionaremos o método initWithMantleModel ao nosso modelo Realm. Aqui está a implementação completa da classe ArticleRealm .

 #import "RLMObject.h" #import "ArticleModel.h" @interface ArticleRealm : RLMObject @property NSString *leadParagraph; @property NSString *url; - (id)initWithMantleModel:(ArticleModel *)articleModel; @end
 #import "ArticleRealm.h" @implementation ArticleRealm - (id)initWithMantleModel:(ArticleModel *)articleModel{ self = [super init]; if(!self) return nil; self.leadParagraph = articleModel.leadParagraph; self.url = articleModel.url; return self; } @end

Interagimos com o banco de dados usando objetos da classe RLMRealm . Podemos facilmente obter um objeto RLMRealm invocando o método “[RLMRealm defaultRealm]”. É importante lembrar que tal objeto é válido apenas dentro do thread em que foi criado e não pode ser compartilhado entre threads. A gravação de dados no Realm é bastante simples. Uma única gravação, ou uma série delas, precisa ser feita em uma transação de gravação. Aqui está um exemplo de gravação no banco de dados:

 RLMRealm *realm = [RLMRealm defaultRealm]; ArticleRealm *articleRealm = [ArticleRealm new]; articleRealm.leadParagraph = @"abc"; articleRealm.url = @"sampleUrl"; [realm beginWriteTransaction]; [realm addObject:articleRealm]; [realm commitWriteTransaction];

O que acontece aqui é o seguinte. Primeiro criamos um objeto RLMRealm para interagir com o banco de dados. Em seguida, um objeto de modelo ArticleRealm é criado (lembre-se de que ele é derivado da classe RLMRealm ). Finalmente, para salvá-lo, uma transação de gravação começa, o objeto é adicionado ao banco de dados e, uma vez salvo, a transação de gravação é confirmada. Como podemos ver, transações de escrita bloqueiam a thread na qual são invocadas. Embora se diga que o Realm é muito rápido, se adicionarmos vários objetos ao banco de dados em uma única transação no thread principal, isso poderá fazer com que a interface do usuário não responda até que a transação seja concluída. Uma solução natural para isso é realizar essa transação de gravação em um thread em segundo plano.

Solicitação de API e resposta persistente no reino

Esta é toda a informação que precisamos para persistir artigos usando o Realm. Vamos tentar realizar uma solicitação de API usando o método

 - (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure

e modelos de solicitação e resposta Mantle para obter artigos do New York Times que tenham alguma relação (como no exemplo anterior) com basquete e tenham sido publicados nos primeiros sete dias de junho de 2015. Assim que a lista de tais artigos estiver disponível, nós irá persistir no Realm. Abaixo está o código que faz isso. Ele é colocado no método viewDidLoad do controlador de exibição de tabela em nosso aplicativo.

 ArticleListRequestModel *requestModel = [ArticleListRequestModel new]; // (1) requestModel.query = @"Basketball"; requestModel.articlesToDate = [[ArticleListRequestModel dateFormatter] dateFromString:@"20150706"]; requestModel.articlesFromDate = [[ArticleListRequestModel dateFormatter] dateFromString:@"20150701"]; [[APIManager sharedManager] getArticlesWithRequestModel:requestModel // (2) success:^(ArticleListResponseModel *responseModel){ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // (3) @autoreleasepool { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; [realm deleteAllObjects]; [realm commitWriteTransaction]; [realm beginWriteTransaction]; for(ArticleModel *article in responseModel.articles){ ArticleRealm *articleRealm = [[ArticleRealm alloc] initWithMantleModel:article]; // (4) [realm addObject:articleRealm]; } [realm commitWriteTransaction]; dispatch_async(dispatch_get_main_queue(), ^{ // (5) RLMRealm *realmMainThread = [RLMRealm defaultRealm]; // (6) RLMResults *articles = [ArticleRealm allObjectsInRealm:realmMainThread]; self.articles = articles; // (7) [self.tableView reloadData]; }); } }); } failure:^(NSError *error) { self.articles = [ArticleRealm allObjects]; [self.tableView reloadData]; }];

Primeiro, é feita uma chamada de API (2) com um modelo de solicitação (1), que retorna um modelo de resposta que contém uma lista de artigos. Para persistir esses artigos usando Realm, precisamos criar objetos de modelo Realm, que ocorrem no loop for (4). Também é importante observar que, como vários objetos persistem em uma única transação de gravação, essa transação de gravação é executada em um thread em segundo plano (3). Agora, uma vez que todos os artigos são salvos em Realm, nós os atribuímos à propriedade de classe self.articles (7). Como eles serão acessados ​​posteriormente no encadeamento principal nos métodos de fonte de dados do TableView, é seguro recuperá-los do banco de dados Realm no encadeamento principal também (5). Novamente, para acessar o banco de dados de um novo encadeamento, um novo objeto RLMRealm precisa ser criado (6) nesse encadeamento.

Se a obtenção de novos artigos da API falhar por qualquer motivo, os existentes serão recuperados do armazenamento local no bloco de falha.

Empacotando

Neste tutorial aprendemos como configurar o Mantle, um framework modelo para Cocoa e Cocoa Touch, para interagir com uma API remota. Também aprendemos como persistir localmente os dados recuperados na forma de objetos de modelo Mantle usando o banco de dados móvel Realm.

Caso você queira experimentar este aplicativo, você pode recuperar o código-fonte de seu repositório GitHub. Você precisará gerar e fornecer sua própria chave de API antes de executar o aplicativo.