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 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!

19 Comentários

  1. Felipe Conde 10/05/2012 at 10:37 #

    Olá Diego.
    Muito boa a solução!
    Ultimamente tenho usado o framework RESTKit. Já ouviu falar no projeto? http://restkit.org/
    Mão na roda também!

    abraço

  2. Diego Chohfi 10/05/2012 at 10:47 #

    Opa Felipe, tudo bom? O RESTkit é muito legal mesmo, diversas facilidades inclusive as mapear um objeto direto para uma representação em JSON.

  3. Witaro 10/05/2012 at 13:38 #

    Pessoal, desculpa a pergunta “retrô”, mas o que vcs recomendam para XML no iOS?

  4. Felipe Benevides 10/05/2012 at 14:18 #

    Excelente post, Diego! Na minha app usei o JSON Framework (faz uns 3 meses) e a performance dele era muito melhor que a do TouchJSON.

    E ah, utilizamos a mesma abordagem de criar um método de classe para receber o NSDictionary e até agora têm sido muito fácil de dar manutenção.

  5. Diego Chohfi 10/05/2012 at 16:49 #

    Opa Witaro, tudo bom? Retrô mas muita gente precisa, né? A Apple tem uma classe para parsear xml: NSXMLParser mas a galera diz que é um pouco chato e ela se compara ao StAX. Porém tem algumas opensource que fazem o trabalho bem mais simples, de uma olhada nessa: https://github.com/nfarina/xmldocument. Abraços.

  6. Witaro 11/05/2012 at 06:54 #

    Valeu, Diego! Vamos comparar ele com o TBXML, que foi bem avaliado aqui:

    http://www.raywenderlich.com/553/how-to-chose-the-best-xml-parser-for-your-iphone-project

  7. Cleverson Sacramento 14/05/2012 at 21:15 #

    Diego, escrevi um post também sobre este assunto. Falei sobre requisições GET, PUT e DELETE usando os recursos nativos do Foundation Framework também.

    Espero que ajude outras pessoas:
    http://cleversonsacramento.com/2012/05/13/objective-c-e-restful-web-services/

  8. Diego Chohfi 15/05/2012 at 10:25 #

    Opa Cleverson, tudo bom? Muito bom seu post cara, sempre bom ajudar a galera 😀

  9. Estagiário 16/05/2012 at 01:10 #

    Muito bom preciso aprender mais sobre desenvolvimento em iOS.

  10. Francisco Martins 30/07/2012 at 21:34 #

    Primeira vez que vi o post, parecia aramaico: impossível de entender. Depois de uma aula, consegui acompanhar quase tudo.

    Mas a idéia é: O curso de iOS da Caelum é muito bom e esse post me será muito útil.

  11. Diego Chohfi 31/07/2012 at 13:39 #

    Opa Francisco, que bom que gostou do post! Fico feliz que as aulas te ajudaram a entender melhor. 😀

  12. Thiago 04/08/2012 at 08:09 #

    Diego, primeiramente seu post é um dos melhores sobre o assunto, pesquisei muito na net e realmente vc esta d parabéns.

    Criei uma página em Asp q me retorna um Json, esta página no micro-01 por ex. Criei minha aplicação no Micro-02 e troquei o localhost do seu código pelo IP do micro-01.

    Nao recebo nada na minha aplicação, você saberia me dizer se eu teria que habilitar algo no Xcode.

    Já desativei o firewall no Micro-01 e consigo acessar a página ASP gerada por ele usando um navegador no Micro-02

  13. Diego Chohfi 06/08/2012 at 17:18 #

    Opa Thiago, tudo bom? Obrigado pelos elogios, fico feliz que tenha gostado. Referente ao problema, inicialmente tente isolar os problemas de rede: suba o projeto e faça a request na sua máquina, localhost mesmo, e veja se da certo.

  14. Raul 07/08/2012 at 09:24 #

    Olá, fiquei com dúvida em relação a url, ela é fake ou disponibilizada pela sptrans?

  15. Diego Chohfi 07/08/2012 at 12:09 #

    Olá Raul, a URL não é fake porém não é disponibilizada pela sptrans. Utilizamos ela no aplicativo do Busao, comentado no post.

  16. Mauro 12/12/2012 at 21:32 #

    Fala ai, parabéns pelo Post, gostaria de saber se consigo fazer o inverso, enviar uma requisição a uma URL com dados JSon para o Servidor recebe-lo. Um POST com JSon.

    Obrigado!

  17. welton 06/02/2013 at 08:41 #

    Amigo estou com um problema:
    no codigo :
    NSString *nome = [resultados objectForKey:@”nome”];
    NSString *descricao = [resultados bjectForKey:@”descricao”];

    retorna um erro de NSArray_ como se o NSDictionary resultados não estivesse identificando o os Key tais como “nome”, “descricao” etc.
    segui com o seguinte URL http://ondeestaoalbi.herokuapp.com/onibusesNosPontosProximos.json?lat=-23.588453&long=-46.632103
    se puder me ajudar ficarei muito agradecido

  18. Lucas Rafael 03/05/2013 at 18:02 #

    Diego, parabéns!
    Ficou muito bem explicado e o mais interessante de tudo é a sua iniciativa de compartilhar gratuitamente o seu conhecimento.
    Obrigado!

Deixe uma resposta