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.