Trabalhando com objetos não gerenciados no Core Data

No mercado mobile é comum a integração com um servidor web utilizando webservices. Normalmente a forma considerada mais simples e eficaz para enviar e receber dados é o formato JSON. No iOS existem ferramentas que facilitam a conversão desse JSON em objetos do Objective-C, inclusive esse assunto já foi abordado aqui no Blog da Caelum: Trabalhando com JSON no iOS.

Considere o seguinte JSON:

[
    {
        "materia": "Matematica",
        "data": "19/06/2013"
    },
    {
        "materia": "Portugues",
        "data": "19/06/2013"
    },
    {
        "materia": "Quimica",
        "data": "20/06/2013"
    }
]

Podemos facilimente converter os dados em uma NSArray de objetos do tipo:

@interface Prova : NSObject
@property (nonatomic, retain) NSString * data;
@property (nonatomic, retain) NSString * materia;
@end

@implementation Prova
@end

Utilizando a biblioteca AFCNetworking para realizar o request teríamos um código parecido com:

AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
        success:^(NSURLRequest *req, NSHTTPURLResponse *res, id JSON) {
            NSDictionary *jsonDict = (NSDictionary *) JSON;
            NSMutableArray *provas = [[NSMutableArray alloc] init];
            
            for(NSDictionary *provaDict in jsonDict) {
                Prova *prova = [Prova new];
                prova.materia = [provaDict objectForKey:@"materia"];
                prova.data = [provaDict objectForKey:@"data"];
                [provas addObject:prova];
            }
        }
        failure:^(NSURLRequest *req, NSHTTPURLResponse *res, NSError *e, id JSON) {} ];

Com um NSArray em mãos fica fácil apresentar os dados para o usuário em um UITableView. Esse procedimento é exatamente o que ocorre no aplicativo BusaoSP da Caelum, que faz a requisição para um servidor web em busca dos pontos próximos dos dispositivo do usuário.

Um dos novos features da nova versão do Busao SP apresenta um pequeno desafio que é o foco desse post: O usuário deseja favoritar um Ônibus para que possa selecioná-lo facilmente no futuro. Quando falamos em persistir dados no iOS sempre podemos optar pelo Core Data.

Podemos configurar nossa aplicação para utilizar Core Data, criando um Data Model para que possamos modelar nossas entidades. A partir do modelo criado no Data Model podemos gerar nossa nova classe prova, que agora herda de NSManagedObject:

@interface Prova : NSManagedObject
@property (nonatomic, retain) NSString * data;
@property (nonatomic, retain) NSString * materia; 
@end

@implementation Prova
@dynamic data;
@dynamic materia;
@end

Se rodarmos agora o mesmo código que recebe o request e cria uma NSArray de provas vamos nos deparar com a seguinte mensagem de erro:

CoreData: error: Failed to call designated initializer on NSManagedObject class 'Prova'

Para podermos instanciar um NSManagedObject precisamos de um NSManagedObjectContext pois esse contexto contém informações das configurações que fizemos no Data Model, necessárias para que o Core Data consiga criar o objeto e gerenciá-lo com sucesso (não podemos esquecer que o Core Data nos dá possibilidade de fazer “undo” em operações, criar relacionamentos etc…).

A partir de agora para criarmos uma instância de Prova podemos fazer:

NSManagedObjectContext context = //pega do AppDelegate
Prova *prova = (Prova*)[NSEntityDescription insertNewObjectForEntityForName: @"Prova"
                                         inManagedObjectContext: context]

O problema dessa abordagem é que se precisarmos salvar alguma informação precisaremos chamar o método “save” do NSManagedObjectContext e por consequência todos objetos gerenciados no contexto serão salvos. No nosso caso isso pode acontecer com todas as provas que criamos a partir do JSON.

Uma das maneiras de sair desse problema é criar 2 classes idênticas com os mesmos atributos uma herdando do NSObject (para ser usada na conversão do JSON em objetos) e outra herdando de NSManagedObject (para salvar apenas os favoritos do JSON). Essa abordagem pode causar complicações especialmente se as classes possuírem regras de negócio além das propriedades.

A outra abordagem é criar um NSManagedObject sem que ele fique no contexto de persistência desde o momento da criação (ou seja “detached” do contexto).

A maneira de fazer isso é criar uma instância de NSEntityDescription para que o Core Data acesse os dados do Data Model, especificando a entidade “Prova” e então criando a entidade a partir do seletor
initWithEntity: insertIntoManagedObjectContext:
Como não queremos o objeto gerenciado podemos passar “nil” no último argumento:

NSManagedObjectContext context = //pega do AppDelegate
NSEntityDescription *entity = [NSEntityDescription entityForName: @"Prova"
                                              inManagedObjectContext:context];
    
Prova *prova = (Prova*)[[NSManagedObject alloc] initWithEntity:entity
                    insertIntoManagedObjectContext:nil];

O resultado é que temos um objeto configurado para o Core Data mais ainda fora do contexto de persistência. Podemos utilizar essa técnica para instanciar os objetos com os dados do JSON. Caso seja necessário favoritar u m dos elementos podemos colocar o objeto no contexto e depois salvar as alterações do contexto:

[context insertObject: umObjetoProvaNaoGerenciado];
//agora podemos salvar o contexto

Por último tempos apenas o problema de isolar esse código. No caso de a mesma aplicação possuir várias entidades pode ser trabalhoso replicar o código que instancia o objeto não gerenciado. Esse é um caso clássico para o uso de Categorias do Objetive-C.

Podemos então criar uma categoria baseada no NSManagedObject para extrair o código anterior:

@implementation NSManagedObject (ComFacilitadores)
+(NSManagedObject*) detachedManagedObjectWithContext:(NSManagedObjectContext*) context{
    NSEntityDescription *entity = [NSEntityDescription entityForName: @"??????"
                                              inManagedObjectContext:context];
    
    return [[NSManagedObject alloc] initWithEntity:entity
                    insertIntoManagedObjectContext:nil];
}

Mas como vamos saber o nome da classe que deve ser instanciada? Como estamos em um método de classe podemos utilizar o NSStringFromClass passando “self”:

    NSEntityDescription *entity = [NSEntityDescription entityForName: NSStringFromClass(self)
                                              inManagedObjectContext:context];

Agora se quisermos criar uma prova com um construtor que pede a data e a matéria:

#import "NSManagedObject+ComFacilitadores.h"
@implementation Prova
//...
+(Prova*) provaWithData: (NSString*) data andMateria: (NSString*) materia 
                              detachedFromContext: (NSManagedObjectContext*) ctx {

    Prova *nova = (Prova*) [self detachedManagedObjectWithContext:ctx];
    nova.data = data;
    nova.materia = materia;
    return nova;
}
@end

Conheça o Busao SP na versões iOS e Android

3 Comentários

  1. Fabio Costa Jr 16/07/2013 at 15:39 #

    Excelente. Esse é um tema complexo que nao achamos muitos posts por ai, ainda mais em pt!

  2. Osni Oliveira 20/07/2013 at 00:34 #

    Excelente! Talvez fosse interessante flexibilizar o nome da entidade, para poder ter nomes de entidade/classe diferentes entre si (é algo relativamente comum).
    Essa abordagem de criar categorias com alguns facilitadores é muito boa e ajuda bastante a reaproveitar código.
    Parabéns pelo post!

  3. Victor 12/02/2014 at 09:15 #

    Obrigado pela ajuda, porém eu tive um problema aqui tentando fazer isso. Na verdade, o problema foi que existe um relacionamento com outro objeto. Por exemplo, é como se a matéria fosse um objeto também e não uma String, vindo do sqlite.
    Se eu instanciar o Prova fora do contexto, buscar um Materia do banco (que vai vir dentro do contexto), quando eu setar o Matéria no Prova vai dar um erro de objetos em contextos diferentes.
    Tem alguma solução pra isso?

Deixe uma resposta