Come avrete intuito dal titolo l’argomento di oggi è il download asincrono delle immagini. Più o meno tutti ci siamo scontrati, nel realizzare le nostre applicazioni per iOS, con la necessità di recuperare informazioni ed immagini da internet e abbiamo scoperto ben presto che quando questa operazione di download non viene gestita correttamente, tutta l’applicazione smette di rispondere fintanto che il download non è terminato.
Il motivo di tutto questo è semplice: in un’applicazione, se non diversamente specificato, tutto il lavoro viene svolto in un unico thread, il quale si occupa, tra l’altro, di aggiornare la GUI (Graphical User Interface) e rispondere agli events dell’utente. Se questo thread viene bloccato da una funzione molto lunga dai calcoli particolarmente complessi o un download da internet (in gergo vengono dette “chiamate bloccanti”) ecco che l’applicazione non è più in grado di svolgere altri compiti e quindi la GUI stessa resta bloccata, dando l’impressione all’utente che l’app abbia qualcosa che non va.
La soluzione è quindi quella di eseguire le chiamate bloccanti in un thread separato, così che il thread principale sia sempre in grado di aggiornare la GUI.
iOS fornisce diversi strumenti per eseguire codice in un thread separato: NSThread, NSOperationQueue, di GDC… tutte tecnologie che in maniera più o meno esplicita creano un thread per eseguirvi all’interno il nostro codice senza bloccare il main thread.
(Oltre alle tecnologie apple esistono inoltre alcuni framework di terze parti come AsiHttpRequest che risultano molto comodi per effettuare richieste asincrone Vedi: http://allseeing-i.com/ASIHTTPRequest/)
Questo è un problema generale che non riguarda solo iOS e le immagini e una soluzione elegante passa per l’implementazione di un design pattern denominato “proxy”. Se caricare un oggetto risulta complesso come in questo caso in cui bisogna creare threads, gestire risposte etc, risulta più comodo ed elegante istanziare un oggetto che faccia proxy che si occupi di tutto questo. Come sempre nella programmazione ad oggetti lo scopo è quello di isolare le responsabilità e delegare compiti lunghi e inclini ad errori ad oggetti che siano testabili e riutilizzabili.
Apple fornisce un ottimo esempio su come dovrebbe essere gestito il lazy loading a questo indirizzo
http://developer.apple.com/library/ios/#samplecode/LazyTableImages/Introduction/Intro.html
La classe IconDownloader di questo esempio può a ragione essere considerata una classe Proxy per il download delle immagini da internet.
Ovviamente nulla da dire sul codice apple, è ottimo e fonte di ispirazione, ma mi sono chiesto se per problemi più semplici non si potesse pensare ad una soluzione più semplice. Ed è per questo che ho provato a scrivere la classe che vedrete di seguito nell’articolo.
La nostra classe “AutoDownloadImageView”
L’idea di base è semplice: creare una classe da poter usare come una normale UIImageview che si occupi in autonomia di eseguire il download dell’immagine da internet. Quello che vorrei realizzare è una classe proxy per una UIImageview che sia lei stessa una UIImageview.
La classe è ancora in fase di sviluppo, forse ci sono degli errori e sicuramente è migliorabile ma quello che è importante è capire l’idea che ci sta dietro.
La classe si chiama AutoDownloadImageView e prima di pensare alla sua implementazione ho deciso che le istanze di questa classe comunicheranno al loro creatore l’esito del download dell’immagine, positivo o negativo che sia, in modo tale che il creatore possa comportarsi di conseguenza. Questo in genere viene fatto tramite l’istituzione di un protocollo. Se non sapete perché potete leggere questo nostro articolo http://www.devapp.it/wordpress/l014-un-contratto-tra-la-gli-oggetti-il-protocol.html
Il protocollo prevede due metodi, uno viene richiamato se il download fallisce, l’altro se tutto va a buon fine. Questa è la dichiarazione del protocollo:
@class AutoDownloadImageView;
/**
* Questo è il protocollo che dovrà implementare la classe che utilizza
* oggetti di tipo AutoDownloadImageView contiene due metodi utili per eseguire
* ad esempio un refresh dell'interfaccia
*/
@protocol AutoDownloadImageViewDelegate
/**
* Il download è completato con successo. Il ricevente può implementare questo metodo
* eseguendo un refresh dello schermo.
*/
- (void)autoDownloadImageViewFinishDownloading:(AutoDownloadImageView *)autoDowload;
/**
* Il download è terminato con un errore, Il ricevente può eseguire l'operazione che
* ritiene opportuna.
*/
- (void)autoDownload:(AutoDownloadImageView *)autoDowload didFailWithError:(NSError *)error;
@end
Per quanto riguarda le variabili di istanza della classe, dobbiamo aggiungere:
- Una variabile NSURL per memorizzare l’url dell’immagine da scaricare
- Una variabile di tipo id che implementi il protocollo AutoDownloadImageViewDelegate al quale comunicare l’esito del download
- Una variabile di tipo NSString per memorizzare il nome dell’immagine (non è strettamente necessaria ma ci aiuta
- Un buffer di memoria dove salvare l’immagine durante il processo di download
Oltre le variabili elencate ne serviranno altre due per una funzione che ritengo molto importante: vorrei che la classe sia in grado di memorizzare sul filesystem l’immagine appena scaricata così che non sia necessario scaricarla nuovamente. Questo comportamento verrà gestito da una variabile di tipo BOOL.
Ci potrebbe essere però la necessità di forzare il download di una nuova immagine da internet con lo stesso nome di una che abbiamo già salvato sul filesystem e per queste occasioni aggiungiamo un’ulteriore variabile di tipo BOOL che se impostata a TRUE farà si che la classe ignorerà qualsiasi file già salvato e procederà con un nuovo download.
La dichiarazione della classe
Ecco quindi la dichiarazione della classe:
@interface AutoDownloadImageView : UIImageView {
/**
* La url dal quale scaricare l'immagine
*/
NSURL *imageURL;
/**
* il delegate al quale verranno inviati i messaggi
*/
id <NSObject, AutoDownloadImageViewDelegate> delegate;
/**
* Un buffer dove memorizzare i dati scaricati da internet
*/
NSMutableData *receivedData;
/**
* Se questa variabile è settata a TRUE l'immagine scaricata
* viene memorizzata nella cartella Documents dell'applicazione
* viceversa l'immagine viene cancellata.
*/
BOOL persistency;
/**
* Se questa variabile è settata a TRUE viene sempre tentato il dowload
* dell'immagine da internet, viceversa viene cercata un'immagine con lo stesso
* nome nella cartella Documents dell'applicazione
* Questo sistema di confronto è piuttosto debole, bisognerebbe aggiungere
* della logica per gestire due file con lo stesso nome
*/
BOOL forceDownload;
/**
* Il nome del file
*/
NSString *filename;
}
@property (nonatomic, retain) NSMutableData *receivedData;
@property (nonatomic, assign) id <NSObject, AutoDownloadImageViewDelegate> delegate;
L’implementazione della nostra classe
Passiamo quindi all’implementazione della classe.
Ho creato un metodo init personalizzato, con il quale è possibile settare direttamente tutte le variabili di istanza. Purtroppo questo significa che se una istanza della classe venisse creata usando un metodo preesistente come init o initWithFrame: la classe avrebbe un comportamento anomalo e questo non è accettabile per una versione release, ma questa classe che stiamo realizzando ha scopo didattico quindi proseguiamo pure.
Questo è il codice del metodo init personalizzato:
-(id)initWithFrame:(CGRect)frame
URL:(NSURL *)url
persistency:(BOOL)persist
forceDownload:(BOOL)force
{
self = [super initWithFrame:frame];
if (self) {
imageURL = [url retain];
persistency = persist;
forceDownload = force;
/**
* Imposto la proprietà image usanto una immagine
* placeholder.
* Questo approccio è migliorabile, generando una view a runtime al posto di una
* jpg
*/
self.image = [UIImage imageNamed:@"unavailable_image.jpeg"];
filename = [[url lastPathComponent] retain];
[self startDownload];
}
return self;
}
Il codice di questo metodo è piuttosto semplice, imposto le variabili di istanza, setto la proprietà image utilizzando un’immagine placeholder e richiamo il metodo startDownload.
Cosa farà il metodo startDownload?
Verifico prima di tutto se è possibile utilizzare un’immagine prelevandola dalla cache:
-(void)startDownload{
/**
* Se non devo necessariamente scaricare provo a verificare
* se il file è già stato scaricato sul filesystem
*/
if (! forceDownload) {
NSString *documentsPath =
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *fileFullPath = [documentsPath stringByAppendingPathComponent:filename];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:fileFullPath]) {
[self setImage:[UIImage imageWithContentsOfFile:fileFullPath]];
NSLog(@"Image loaded from disk");
return;
}
}
Se non ho trovato nessuna immagine con lo stesso nome sul filesystem oppure se è necessario eseguire un nuovo download creo la connessione:
/**
* creo la connessione per il download dell'immagine
*/
NSURLRequest *req = [[NSURLRequest alloc] initWithURL:imageURL];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:req delegate:self startImmediately:NO];
[conn scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[conn start];
Per chi non avesse mai incontrato oggetti di tipo NSURLConnection diciamo che sono oggetti che si occupano di eseguire una NSURLRequest verso una URL e utilizzano il patter delegate per comunicare lo stato di avanzamento del download. Devo però verificare se la connessione è andata a buon fine e se così non fosse dovrò informare il delegate.
if (conn) {
NSMutableData *data = [[NSMutableData alloc] init];
self.receivedData = data;
[data release];
}
else {
NSError *error = [NSError errorWithDomain:AutoDownloadImageViewErrorDomain
code:AutoDownloadImageViewErrorNoConnection
userInfo:nil];
/**
* Invio al delegate il messaggio che il download non è andato a buon fine.
*/
if ([self.delegate respondsToSelector:@selector(autoDownload:didFailWithError:)])
[delegate autoDownload:self didFailWithError:error];
}
[req release];
}
Restano da esaminare i metodi dell NSURLConnectionDelegateProtocol.
Questo metodo viene richiamato quando la connessione si avvia. Lo utilizzo per inizializzare il buffer dove memorizzare l’immagine:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[receivedData setLength:0];
}
Questo viene richiamato ogni qualvolta vengono ricevuti dei byte dalla connessione, mi preoccupo soltanto di accodarli a quelli già ricevuti:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[receivedData appendData:data];
}
Questo metodo viene richiamato se la connessione termina con un codice d’errore, invoco quindi il metodo opportuno sul delegate per informarlo dell’accaduto:
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
[connection release];
/**
* Invio al delegate il messaggio che il download non è andato a buon fine.
*/
if ([delegate respondsToSelector:@selector(autoDownload:didFailWithError:)])
[delegate autoDownload:self didFailWithError:error];
}
L’ultimo metodo viene richiamato quando la connection ha scaricato tutto il file. In questo caso sostituisco l’immagine placeholder con quella appena scaricata, verifico se è necessario salvare il file ed eventualmente lo salvo. Infine avviso il delegate che tutto è avvenuto secondo i piani.
Il delegate potrebbe ad esempio intercettare questa chiamata per eseguire un refresh dello schermo.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
/**
* Imposto la proprietà image con l'immagine scaricata da internet
* questo spesso è sufficiente affinché venga visualizzata
* l'immagine corretta, se così non fosse il delegate dovrà invocare un setNeedDisplay
*/
self.image = [UIImage imageWithData:receivedData];
/**
* Salvo l'immagine nel filesystem se è stato specificato.
*/
if (persistency) {
NSString *documentsPath =
[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *dbFullPath = [documentsPath stringByAppendingPathComponent:filename];
[receivedData writeToFile:dbFullPath atomically:YES];
}
/**
* Invio al delegate il messaggio che il download è stato completato.
*/
if ([delegate respondsToSelector:@selector(autoDownloadImageViewFinishDownloading:)])
[delegate autoDownloadImageViewFinishDownloading:self];
[connection release];
self.receivedData = nil;
}
Un esempio di utilizzo della nostra nuova classe
Spero che il codice sia abbastanza auto-esplicativo, un esempio di utilizzo alle volte vale più di mille parole:
NSString *str = @"http://www.brilliantstudent.in/blog/wp-content/uploads/2009/03/erdos1.jpg";
NSURL *url = [NSURL URLWithString:str];
AutoDownloadImageView *img = [[AutoDownloadImageView alloc]
initWithFrame:CGRectMake(0, 0, 100, 100)
URL:url
persistency:YES
forceDownload:NO
];
[img setDelegate:self];
[self.view addSubview:img];
Come potete notare le istanze della classe AutoDownloadImageView si usano come e dove si usano le UIImageview senza doversi preoccupare di altro, più semplice di così!
Basta aggiungere una istanza della classe AutoDownloadImageView dove vorremmo apparisse l’immagine e fornirle l’url dell’immagine da scaricare, tutto qui.
Ogni suggerimento su come modificare la classe è ben accetto, trovate il progetto su github a questo indirizzo.
Buona programmazione!









9 Responses to “T#102 – Download asincrono delle immagini con il proxy design pattern”
17 Ottobre 2011
MacMomoQuello di usare i nomi delle immagini per la cache delle stesse non mi sembra molto funzionale. Ci potrebbero essere molti siti che usano nomi semplici (tipo 001, logo, ecc.) e magari duplicati con altri siti.
Una soluzione che mi viene in mente è quella salvare ogni nuova immagine con un semplice numero progressivo, e usare un file plist dove registrare direttamente l’url completo dell’immagine e il riferimento alla relativa immagine salvata.
Per il problema dei metodi di init ereditati potresti fare in questo modo:
Negli init non fai altro che impostare l’immagine con il placeholder, in modo da salvare eventuali usi impropri della classe, poi per chi vuole aggiungi anche un metodo setUrl: dove eventualmente lanci il download dell’immagine richiesta.
Per i metodi delegati, secondo me potrebbe essere più comodo avere un unico metodo, che ritorni un bool per sapere se il downoad è andato a buon fine o meno.
In questo modo basterebbe un if con i relativi comportamenti, piuttosto che andare a implementare due metodi differenti per scopi che sono molto vicini.
Per il resto articolo molto interessante e sicuramente utile, concettualmente anche per altri casi, tipo la visualizzazione delle anteprime di file molto pesanti.
18 Ottobre 2011
Ignaziocconcordo su tutta la linea
soprattutto sull’idea del “setUrl” perché ho notato (con mia sorpresa) che il framework AFNetwork ha una category su uiimageview che usa proprio questo approccio.
Come dici tu questo approccio è utilizzabile anche in altri casi, supponi ad esempio di dover costruire un oggetto molto complesso la cui creazione dipende da dati via internet, calcoli in locale e altre cose…con il proxy puoi gestirlo come se fosse “già pronto” senza curarti dei dettagli 🙂 fico, no? si vede che mi sto appassionando ai design patterns? 😀
ps: il progetto è su git, miglioriamolo!
18 Ottobre 2011
milonetottimissimo!!! bravo, bel tutorial.. da neofita non saprei come migliorarlo ma vedo che ti hanno già dato qualche dritta.. complimenti!
24 Gennaio 2012
luca..mi chiedevo.. ma esiste una versione funzionante per ios5? (con storyboard intendo..) 🙂
24 Gennaio 2012
Ignaziocperché non dovrebbe essere compatibile con iOS5? è una subclass di UIImageView e la puoi sare dove vuoi, anche su storyboard.
26 Gennaio 2012
lucaGrazie 1000! Provo immediatamente allora :DDDD
27 Gennaio 2012
luca(03:06) mmm.. provato e riprovato con storyboard..
ma nulla! sono proprio un niubbo!!
Ho tolto tutte le “retain” per ARC.. ecc ecc.. ma nulla.. schermata nera 🙁 .. non è che mi potresti condividere su github una versione per storyboard..
27 Gennaio 2012
luca(h 3:34) Chi la dura la vince!!!!!!!!! Adesso gira tutto alla perfezione! .. devo farti ancora i miei complimenti per questi utilissimi tutorial !!
27 Gennaio 2012
Ignaziocgrazie! fa sempre piacere vedere riconosciuti i propri sforzi. Se proprio vuoi strafare metti un bel link a devapp nell’applicazione 😉