Trabalhando com JSON no iOS
Com o crescimento do mercado mobile, a integração de sistemas com os pequenos aparelhos se tornou essencial. O formato JSON se popularizou com a mesma velocidade que o desenvolvimento mobile. Sua simplicidade e facilidade de desenvolver parsers o tornou uma das principais alternativas na integração de sistemas web, em especial para ser consumido via AJAX.
No iOS não foi diferente. A necessidade dos desenvolvedores levou o surgimento de diversas bibliotecas que faziam o trabalho de ler e gerar JSON com extrema facilidade, como o JSON Framework e o TouchJSON. Porém a Apple não quis ficar de fora e incorporou a classe NSJSONSerialization
, capaz de gerenciar o formato tanto no iOS como no desenvolvimento para OSX, com a mesma facilidade que as bibliotecas opensource trouxeram.
Recetemente desenvolvemos um projeto para iOS para auxiliar os alunos do nosso curso de iOS, o IP-67, enriquecendo a experiência e trazendo um exemplo bem palpável. A aplicação consiste em buscar os pontos de ônibus próximos a localização do usuário. Para integrar a nossa aplicação com um backend em Ruby utilizamos JSON como o formato e a classe NSJSONSerialization
se mostrou extremamente simples e fácil de se trabalhar. Vamos entender como fizemos, do lado do dispositivo, a leitura e manipulação dos dados.
Primeiramente precisamos obter as informações no formato JSON para então parsea-los. Utilizando o método + (id)dataWithContentsOfURL:(NSURL *)url;
da classe NSData para fazer uma request e obter as informações de forma síncrona em um endereço da WEB:
NSString \*url = ```NSString stringWithFormat:@"http://ondeestaoalbi.herokuapp.com/onibusesNosPontosProximos.json?lat=%f&long=%f", -23.588453, -46.632103
; NSData \*jsonData = ```NSData dataWithContentsOfURL: \[NSURL URLWithString:url
\];
O nosso serviço retornará um JSON com a seguinte estrutura:
{ "nome": "920016054", "descricao": "R. Dr. Neto De Araujo, 311", "coordenada": { "latitude": -23.5876, "longitude": -46.6321 } }
Após receber as informações, vamos utilizar a classe NSJSONSerialization
para transformar o JSON em um NSDictionary, contendo as chaves e valores das informações sobre o ponto.
NSError\* error; NSDictionary \*resultados = ```NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error
;
Caso o JSON seja válido e a conversão seja feita com sucesso podemos buscar os valores no NSDictionary
a partir de uma chave, utilizando o método - (id)objectForKey:(id)aKey;
:
NSError \*error; NSDictionary \*resultados = ```NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error
; if(!error) { NSString \*nome = ```resultados objectForKey:@"nome"
; NSString \*descricao = ```resultados objectForKey:@"descricao"
; NSLog(@"Nome: %@, descrição: %@", nome, descricao); }
Poderíamos continuar utilizando o NSDictionary
para armazenar as informações dos pontos encontrados, mas imagine que por algum motivo precisemos mostrar a descrição do nosso Ponto sempre com letra minúscula e removendo os espaços do começo e do fim da nossa NSString, o código ficaria da seguinte forma:
NSString \*nome = ```resultados objectForKey:@"nome"
; NSString \*descricao = ```\[resultados objectForKey:@"descricao"
lowercaseString\]; descricao = ```descricao stringByTrimmingCharactersInSet:\[NSCharacterSet whitespaceAndNewlineCharacterSet
\]; NSLog(@"Nome: %@, descrição: %@", nome, descricao);
Perceba que o código não é mais tão simples quanto anteriormente, além disso, precisaríamos espalhá-lo por todo o projeto. A manutenção também seria difícil, pois precisaremos copiar e colar o mesmo código toda vez que a descrição for exibida e caso a regra de exibição seja alterada o código que copiamos e colamos terá que ser alterado.
Temos esse problema pois estamos utilizando a classe NSDictionary
para guardar estado do nosso Ponto
e não podemos definir nenhum comportamento adicional. Para manter os dados organizados e de fácil manutenção devemos sempre criar classes para representar tipos e não mantê-los na representação de chave e valor, desta forma, além de guardar o estado do objeto, podemos também definir novos comportamentos, facilitando a manutenção e a reutilização de código.
Vamos criar as classes com os referentes atributos e também definir um método de classe para criar uma instância do Ponto
, esse método será responsável por buscar suas informações em um NSDictionary
passado como argumento. Essa regra fica isolada e fácil de dar manutenção caso alguma das chaves mude:
#import <Foundation/Foundation.h> #import "Coordenada.h"
@interface Ponto : NSObject
@property(nonatomic, strong) NSString \*nome; @property(nonatomic, strong) NSString \*descricao; @property(nonatomic, strong) Coordenada \*coordenada;
\+ (Ponto \*) comDicionario: (NSDictionary \*) dicionario;
@end
#import "Ponto.h"
@implementation Ponto @synthesize nome, descricao, coordenada;
\+ (Ponto \*) comDicionario: (NSDictionary \*) dicionario { Ponto \* ponto = ```\[Ponto alloc
init\]; ```ponto setNome: \[dicionario objectForKey:@"nome"
\]; ```ponto setDescricao: \[dicionario objectForKey:@"descricao"
\]; Coordenada \*coordenada = ```Coordenada comDicionario: \[dicionario objectForKey: @"coordenada"
\]; ```ponto setCoordenada: coordenada
; return ponto; }
@end
Se fosse preciso definir uma regra para buscar a descrição do Ponto
da forma que descrevemos anteriormente, podemos alterar o método que busca o atributo na classe, isolando o código e facilitando futuras manutenções.
#import "Ponto.h"
@implementation Ponto @synthesize nome, descricao, coordenada;
\- (NSString \*) descricao { NSString \*descricaoEmMinusculo = ```descricao lowercaseString
; return ```descricaoEmMinusculo stringByTrimmingCharactersInSet:\[NSCharacterSet whitespaceAndNewlineCharacterSet
\]; }
@end
Para obter um Ponto a partir de um JSON chegamos ao seguinte código:
NSString \*url = ```NSString stringWithFormat:@"http://ondeestaoalbi.herokuapp.com/onibusesNosPontosProximos.json?lat=%f&long=%f", -23.588453, -46.632103
; NSData \*jsonData = ```NSData dataWithContentsOfURL: \[NSURL URLWithString:url
\];
NSError \*error; NSDictionary \*resultados = ```NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&error
; if(!error){ Ponto \*ponto = ```Ponto comDicionario: resultados
; NSLog(@"Ponto: %@", ponto.descricao); }
Percebendo a dificuldade e a duplicação de código que temos que escrever ao buscar os valores no NSDictionary
, desenvolvemos uma pequena biblioteca para facilitar essa tarefa. O [KeyValueObjectMapping](https://github.com/dchohfi/KeyValueObjectMapping)
surgiu da necessidade de transformar grandes estruturas de dados automaticamente em objetos, seguindo padrões e quando necessário adicionando configurações para alterar as convenções. Poderíamos remover a implementação do método + (Ponto *) comDicionario: (NSDictionary *) dicionario;
e com o KeyValueObjectMapping
teríamos:
DCKeyValueObjectMapping \*parser = ```DCKeyValueObjectMapping mapperForClass:\[Ponto class
\]; Ponto \*ponto = ```parser parseDictionary:resultados
;
Lembrando que o KeyValueObjectMapping
serve para transformar qualquer tipo de dado que se comporte como um NSDictionary
, chave e valor, podendo ser um JSON, um PLIST, entre outros. Você está convidado a contribuir nos projetos!