24 Jan, 2011 11:20

Carregando imagens da Internet em uma UIImageView

Ou seja, algo assim:

//Carregando imagem - primeira tentativa
NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://www.mobits.com.br/assets/2010/10/13/portfolio_puzzle.png"]];
UIImage *imagem = [[UIImage alloc] initWithData:data];
imageView.image = imagem; //imageView é uma UIImageView previamente instanciada

O código acima funcionará como esperado, contudo tem uma limitação que pode ser muito séria: a chamada [NSData dataWithContentsOfURL:...] é processada sincronamente, ou seja, a execução do método acima ficará bloqueada até que os dados da imagem sejam baixados completamente. O resultado é óbvio: o processamento da sua interface ficará travado enquanto a imagem for carregada. Pior, imagine que você está montando uma UITableView cujas células apresentam diversas dessas imagens. A navegação nesta tabela será extremamente desagradável.

Carregando em background

Naturalmente, a solução para problemas deste tipo é resolvida através de algum mecanismo de processamento em paralelo. Em Objective C existem diversas maneiras de rodar tarefas em background, desde usando o simples performSelectorInBackground:withObject: até implementando sua subclasse de NSThread.

A solução que escolho é semelhante à apresentada pelo iCodeBlog: NSOperation e NSOperationQueue.

As classes acima simplificam absurdamente o trabalho de disparar novas threads e ainda gerenciam as autorelease pools internamente, o que torna a nossa vida muita mais fácil.

UIImageViewRemota

Meu objetivo é exibir um spinner (UIActivityIndicatorView) sobre a imageView enquanto a imagem é carregada e, após o carregamento, esconder o spinner e exibir a imagem. Para encapsular toda essa lógica, decidi criar uma subclasse de UIImageView e chamá-la de UIImageViewRemota.

Para a interface, vamos ter:

#import <UIKit/UIKit.h>

@interface UIImageViewRemota : UIImageView {
    UIActivityIndicatorView *spinner; //spinner sobre a imageView enquanto a imagem é carregada
    NSString *enderecoImagem; //endereço http da imagem a ser carregada
    NSOperationQueue *queue; //fila de operações para tratar o carregamento em paralelo
}
@property (nonatomic, copy) NSString *enderecoImagem;
@end

Na implementação, teremos várias etapas. A primeira será preparar o spinner sobre a nossa UIImageView.

- (id)initWithFrame:(CGRect)rect {
    if (self = [super initWithFrame:rect]) {
        [self criaSpinner];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)coder {
    if (self = [super initWithCoder:coder]) {
        [self criaSpinner];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    spinner.center = [self convertPoint:self.center fromView:self.superview];
}

- (void)criaSpinner {
    spinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray];
    [self addSubview:spinner];
}

Repare na implementação de initWithFrame: e initWithCoder:. Ambos chamam criaSpinner. Eu fiz desta maneira, pois quero permitir que uma UIImageViewRemota seja definida tanto via código quanto via Xib.
OBS: normalmente, eu evitaria essa pequena duplicação implementando o método drawRect:. Contudo, de acordo com a documentação, UIImageView não chama este método em sua implementação padrão, logo não adianta sobrescrevê-lo. Se alguém tiver uma técnica mais simples, me avise!

Agora, vamos finalmente ao carregamento da imagem.

- (void)setEnderecoImagem:(NSString *)endereco {
    if ([endereco length] == 0) { //caso o endereco seja nulo ou uma string vazia, remove a imagem atual da imageView
        [enderecoImagem release];
        enderecoImagem = nil;
        self.image = nil;
    }
    else if (endereco != enderecoImagem) { //se o endereco for diferente do atual, atualiza a imagem
        [enderecoImagem release];
        enderecoImagem = [endereco copy];

        self.image = nil; //remove a imagem atual para evitar superposições 

        if (!queue) {
            queue = [[NSOperationQueue alloc] init];
        }

        [queue cancelAllOperations]; //cancela qualquer tarefa de carregamento pendente (evita que chamadas consecutivas atrasem o carregamento inutilmente)

        [spinner startAnimating]; //dispara o spinner

        NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(carregaImagemEmBackground) object:nil];
        [queue addOperation:operation]; //envia a tarefa para ser executada em paralelo
        [operation release];
    }
}

- (void)carregaImagemEmBackground {
    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:enderecoImagem]];
    UIImage *imagem = [[UIImage alloc] initWithData:data];
    [self performSelectorOnMainThread:@selector(exibeImagemCarregada:) withObject:imagem waitUntilDone:YES]; //solicita a main thread que exiba a imagem e interrompa o spinner
    [imagem release];
}

- (void)exibeImagemCarregada:(UIImage *)imagem {
    self.image = imagem;
    [spinner stopAnimating];
}

Aí está! O truque por trás do código acima é fazer o carregamento da imagem fora da main thread (responsável por todo o tratamento da interface com o usário) e, após este carregamento, voltar à main thread para exibir a imagem e parar o spinner.

Importando UIImageViewRemota em seu controller, carregar e apresentar imagens da Internet ficará tão simples como:

imageView.enderecoImagem = @"http://www.mobits.com.br/assets/2010/10/13/portfolio_puzzle.png";

Você pode baixar o código da UIImageViewRemota para colocar em seu projeto ou ainda baixar este projeto de exemplo para explorar seu funcionamento.

Próximo tópico: Cache

A solução acima não estaria completa sem um bom mecanismo de cache, pois, dependendo da quantidade de imagens que sua aplicação poderá manipular, a ida à Internet para buscar várias vezes a mesma imagem pode começar a aborrecer. Em breve, abordarei este assunto. Fique ligado no nosso RSS.

UPDATE: Se você quiser aplicar a técnica acima adicionando cache, leia o post mais novo.

Ao navegar neste site, você consente o uso de cookies nossos e de terceiros, que coletam informações anônimas e são essenciais para melhorar sua experiência em nosso site.