{"id":7793,"date":"2011-10-17T11:21:58","date_gmt":"2011-10-17T09:21:58","guid":{"rendered":"http:\/\/www.devapp.it\/wordpress\/?p=7793"},"modified":"2011-10-17T11:21:58","modified_gmt":"2011-10-17T09:21:58","slug":"t102-download-asincrono-delle-immagini-con-il-proxy-design-pattern","status":"publish","type":"post","link":"https:\/\/www.devapp.it\/wordpress\/t102-download-asincrono-delle-immagini-con-il-proxy-design-pattern\/","title":{"rendered":"T#102 &#8211; Download asincrono delle immagini con il proxy design pattern"},"content":{"rendered":"<p><a href=\"http:\/\/www.devapp.it\/wordpress\/wp-content\/uploads\/2011\/10\/tutorial-advanced-devAPP.jpg\"><img loading=\"lazy\" decoding=\"async\" src=\"http:\/\/www.devapp.it\/wordpress\/wp-content\/uploads\/2011\/10\/tutorial-advanced-devAPP.jpg\" alt=\"tutorial-iOS-dev-advanced-devAPP\" title=\"tutorial-advanced-devAPP\" width=\"200\" height=\"100\" class=\"alignleft size-full wp-image-7860\" \/><\/a> Come avrete intuito dal titolo l&#8217;argomento di oggi \u00e8 il download asincrono delle immagini. Pi\u00f9 o meno tutti ci siamo scontrati, nel realizzare le nostre applicazioni per iOS, con la necessit\u00e0 di recuperare informazioni ed immagini da internet e abbiamo scoperto ben presto che quando questa operazione di download non viene gestita correttamente, tutta l&#8217;applicazione smette di rispondere fintanto che il download non \u00e8 terminato.<\/p>\n<p>Il motivo di tutto questo \u00e8 semplice: in un&#8217;applicazione, se non diversamente specificato, tutto il lavoro viene svolto in un unico thread, il quale si occupa, tra l&#8217;altro, di aggiornare la GUI (Graphical User Interface) e rispondere agli events dell&#8217;utente. Se questo thread viene bloccato da una funzione molto lunga dai calcoli particolarmente complessi o un download da internet (in gergo vengono dette &#8220;chiamate bloccanti&#8221;) ecco che l&#8217;applicazione non \u00e8 pi\u00f9 in grado di svolgere altri compiti e quindi la GUI stessa resta bloccata, dando l&#8217;impressione all&#8217;utente che l&#8217;app abbia qualcosa che non va.<!--more--><\/p>\n<p>La soluzione \u00e8 quindi quella di eseguire le chiamate bloccanti in un thread separato, cos\u00ec che il thread principale sia <strong>sempre<\/strong> in grado di aggiornare la GUI.<\/p>\n<p>iOS fornisce diversi strumenti per eseguire codice in un thread separato: NSThread, NSOperationQueue, di GDC&#8230; tutte tecnologie che in maniera pi\u00f9 o meno esplicita creano un thread per eseguirvi all&#8217;interno il nostro codice senza bloccare il main thread.<br \/>\n(Oltre alle tecnologie apple esistono inoltre alcuni framework di terze parti come AsiHttpRequest che risultano molto comodi per effettuare richieste asincrone Vedi: <a href=\"http:\/\/allseeing-i.com\/ASIHTTPRequest\/\" target=\"_blank\">http:\/\/allseeing-i.com\/ASIHTTPRequest\/<\/a>)<\/p>\n<p>Questo \u00e8 un problema generale che non riguarda solo iOS e le immagini e una soluzione elegante passa per l&#8217;implementazione di un design pattern denominato &#8220;proxy&#8221;. Se caricare un oggetto risulta complesso come in questo caso in cui bisogna creare threads, gestire risposte etc, risulta pi\u00f9 comodo ed elegante istanziare un oggetto che faccia proxy che si occupi di tutto questo. Come sempre nella programmazione ad oggetti lo scopo \u00e8 quello di isolare le responsabilit\u00e0 e delegare compiti lunghi e inclini ad errori ad oggetti che siano testabili e riutilizzabili. <\/p>\n<p>Apple fornisce un ottimo esempio su come dovrebbe essere gestito il lazy loading a questo indirizzo<br \/>\n<a href=\"http:\/\/developer.apple.com\/library\/ios\/#samplecode\/LazyTableImages\/Introduction\/Intro.html\" target=\"_blank\">http:\/\/developer.apple.com\/library\/ios\/#samplecode\/LazyTableImages\/Introduction\/Intro.html<\/a><br \/>\nLa classe IconDownloader di questo esempio pu\u00f2 a ragione essere considerata una classe Proxy per il download delle immagini da internet.<\/p>\n<p>Ovviamente nulla da dire sul codice apple, \u00e8 ottimo e fonte di ispirazione, ma mi sono chiesto se per problemi pi\u00f9 semplici non si potesse pensare ad una soluzione pi\u00f9 semplice. Ed \u00e8 per questo che ho provato a scrivere la classe che vedrete di seguito nell&#8217;articolo.<\/p>\n<h4>La nostra classe &#8220;AutoDownloadImageView&#8221;<\/h4>\n<p>L&#8217;idea di base \u00e8 semplice: creare una classe da poter usare come una normale UIImageview che si occupi in <strong>autonomia<\/strong> di eseguire il download dell&#8217;immagine da internet. Quello che vorrei realizzare \u00e8 una classe proxy per una UIImageview che sia lei stessa una UIImageview.<\/p>\n<p>La classe \u00e8 ancora in fase di sviluppo, forse ci sono degli errori e sicuramente \u00e8 migliorabile ma quello che \u00e8 importante \u00e8 capire l&#8217;idea che ci sta dietro.<\/p>\n<p>La classe si chiama <em>AutoDownloadImageView<\/em> e prima di pensare alla sua implementazione ho deciso che le istanze di questa classe comunicheranno al loro creatore l&#8217;esito del download dell&#8217;immagine, positivo o negativo che sia, in modo tale che il creatore possa comportarsi di conseguenza. Questo in genere viene fatto tramite l&#8217;istituzione di un protocollo. Se non sapete perch\u00e9 potete leggere questo nostro articolo <a href=\"http:\/\/www.devapp.it\/wordpress\/l014-un-contratto-tra-la-gli-oggetti-il-protocol.html\" target=\"_blank\">http:\/\/www.devapp.it\/wordpress\/l014-un-contratto-tra-la-gli-oggetti-il-protocol.html<\/a><\/p>\n<p>Il protocollo prevede due metodi, uno viene richiamato se il download fallisce, l&#8217;altro se tutto va a buon fine. Questa \u00e8 la dichiarazione del protocollo:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n@class AutoDownloadImageView;\r\n\/**\r\n * Questo \u00e8 il protocollo che dovr\u00e0 implementare la classe che utilizza\r\n * oggetti di tipo AutoDownloadImageView contiene due metodi utili per eseguire\r\n * ad esempio un refresh dell'interfaccia\r\n *\/\r\n@protocol AutoDownloadImageViewDelegate\r\n\/**\r\n * Il download \u00e8 completato con successo. Il ricevente pu\u00f2 implementare questo metodo\r\n * eseguendo un refresh dello schermo.\r\n *\/\r\n- (void)autoDownloadImageViewFinishDownloading:(AutoDownloadImageView *)autoDowload;\r\n\r\n\/**\r\n * Il download \u00e8 terminato con un errore, Il ricevente pu\u00f2 eseguire l'operazione che \r\n * ritiene opportuna.\r\n *\/\r\n- (void)autoDownload:(AutoDownloadImageView *)autoDowload didFailWithError:(NSError *)error;\r\n@end\r\n<\/pre>\n<p>Per quanto riguarda le variabili di istanza della classe, dobbiamo aggiungere:<\/p>\n<ul>\n<li>Una variabile NSURL per memorizzare l&#8217;url dell&#8217;immagine da scaricare<\/li>\n<li>Una variabile di tipo id che implementi il protocollo AutoDownloadImageViewDelegate al quale comunicare l&#8217;esito del download<\/li>\n<li>Una variabile di tipo NSString per memorizzare il nome dell&#8217;immagine (non \u00e8 strettamente necessaria ma ci aiuta<\/li>\n<li>Un buffer di memoria dove salvare l&#8217;immagine durante il processo di download<\/li>\n<\/ul>\n<p>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&#8217;immagine appena scaricata cos\u00ec che non sia necessario scaricarla nuovamente. Questo comportamento verr\u00e0 gestito da una variabile di tipo BOOL.<br \/>\nCi potrebbe essere per\u00f2 la necessit\u00e0 di forzare il download di una nuova immagine da internet con lo stesso nome di una che abbiamo gi\u00e0 salvato sul filesystem e per queste occasioni aggiungiamo un&#8217;ulteriore variabile di tipo BOOL che se impostata a TRUE far\u00e0 si che la classe ignorer\u00e0 qualsiasi file gi\u00e0 salvato e proceder\u00e0 con un nuovo download.<\/p>\n<h4>La dichiarazione della classe<\/h4>\n<p>Ecco quindi la dichiarazione della classe:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n@interface AutoDownloadImageView : UIImageView {\r\n\t\/**\r\n\t * La url dal quale scaricare l'immagine\r\n\t *\/\r\n\tNSURL *imageURL;\r\n\t\r\n\t\/**\r\n\t * il delegate al quale verranno inviati i messaggi\r\n\t *\/\r\n\tid &lt;NSObject, AutoDownloadImageViewDelegate&gt;  delegate;\r\n\t\r\n\t\/**\r\n\t * Un buffer dove memorizzare i dati scaricati da internet\r\n\t *\/\r\n\tNSMutableData *receivedData;\r\n\t\r\n\t\/**\r\n\t * Se questa variabile &Atilde;&uml; settata a TRUE l'immagine scaricata\r\n\t * viene memorizzata nella cartella Documents dell'applicazione\r\n\t * viceversa l'immagine viene cancellata.\r\n\t *\/\r\n\tBOOL persistency;\r\n\t\r\n\t\/**\r\n\t * Se questa variabile &Atilde;&uml; settata a TRUE viene sempre tentato il dowload\r\n\t * dell'immagine da internet, viceversa viene cercata un'immagine con lo stesso\r\n\t * nome nella cartella Documents dell'applicazione\r\n\t * Questo sistema di confronto &Atilde;&uml; piuttosto debole, bisognerebbe aggiungere \r\n\t * della logica per gestire due file con lo stesso nome\r\n\t *\/\r\n\tBOOL forceDownload;\r\n\r\n\t\/**\r\n\t * Il nome del file\r\n\t *\/\r\n\tNSString *filename;\r\n}\r\n\r\n@property (nonatomic, retain) NSMutableData *receivedData;\r\n@property (nonatomic, assign) id &lt;NSObject, AutoDownloadImageViewDelegate&gt; delegate;\r\n<\/pre>\n<h4>L&#8217;implementazione della nostra classe<\/h4>\n<p>Passiamo quindi all&#8217;implementazione della classe.<br \/>\nHo creato un metodo init personalizzato, con il quale \u00e8 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 \u00e8 accettabile per una versione <em>release<\/em>, ma questa classe che stiamo realizzando ha scopo didattico quindi proseguiamo pure.<\/p>\n<p>Questo \u00e8 il codice del metodo init personalizzato:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n-(id)initWithFrame:(CGRect)frame\r\n\t\t\tURL:(NSURL *)url\r\n\t   persistency:(BOOL)persist\r\n\t forceDownload:(BOOL)force\r\n{\r\n\tself = [super initWithFrame:frame];\r\n\tif (self) {\r\n\t\timageURL\t\t= [url retain];\r\n\t\tpersistency\t\t= persist;\r\n\t\tforceDownload\t= force;\r\n\t\t\r\n\t\t\/**\r\n\t\t * Imposto la propriet&agrave; image usanto una immagine\r\n\t\t * placeholder.\r\n\t\t * Questo approccio &egrave; migliorabile, generando una view a runtime al posto di una\r\n\t\t * jpg\r\n\t\t *\/\r\n\t\tself.image\t\t= [UIImage imageNamed:@&quot;unavailable_image.jpeg&quot;];\r\n\t\tfilename\t\t= [[url lastPathComponent] retain];\r\n\t\t[self startDownload];\r\n\t}\r\n\treturn self;\r\n}\r\n<\/pre>\n<p>Il codice di questo metodo \u00e8 piuttosto semplice, imposto le variabili di istanza, setto la propriet\u00e0 image utilizzando un&#8217;immagine <em>placeholder<\/em> e richiamo il metodo startDownload.<\/p>\n<p>Cosa far\u00e0 il metodo startDownload?<\/p>\n<p>Verifico prima di tutto se \u00e8 possibile utilizzare un&#8217;immagine prelevandola dalla cache:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n-(void)startDownload{\r\n\t\/**\r\n\t * Se non devo necessariamente scaricare provo a verificare\r\n\t * se il file &egrave; gi&agrave; stato scaricato sul filesystem\r\n\t *\/\r\n\tif (! forceDownload) {\r\n\t\tNSString *documentsPath = \r\n\t\t\r\n\t\t[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];\r\n\t\t\r\n\t\tNSString *fileFullPath = [documentsPath stringByAppendingPathComponent:filename];\r\n\t\tNSFileManager *fileManager = [NSFileManager defaultManager];\r\n\t\tif ([fileManager fileExistsAtPath:fileFullPath]) {\r\n\t\t\t[self setImage:[UIImage imageWithContentsOfFile:fileFullPath]];\r\n\t\t\tNSLog(@&quot;Image loaded from disk&quot;);\r\n\t\t\treturn;\r\n\t\t}\r\n\t}\r\n<\/pre>\n<p>Se non ho trovato nessuna immagine con lo stesso nome sul filesystem oppure se \u00e8 necessario eseguire un nuovo download creo la connessione:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n\t\/**\r\n\t * creo la connessione per il download dell'immagine\r\n\t *\/\r\n\tNSURLRequest *req = [[NSURLRequest alloc] initWithURL:imageURL];\r\n\tNSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:req delegate:self startImmediately:NO];\r\n\t[conn scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];\r\n\t[conn start];\r\n\t\r\n<\/pre>\n<p>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\u00f2 verificare se la connessione \u00e8 andata a buon fine e se cos\u00ec non fosse dovr\u00f2 informare il delegate.<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n        if (conn) {\r\n\t\tNSMutableData *data = [[NSMutableData alloc] init];\r\n\t\tself.receivedData = data;\r\n\t\t[data release];\r\n\t}\r\n\telse {\r\n\t\tNSError *error = [NSError errorWithDomain:AutoDownloadImageViewErrorDomain \r\n\t\t\t\t\t\t\t\t\t\t\t code:AutoDownloadImageViewErrorNoConnection \r\n\t\t\t\t\t\t\t\t\t\t userInfo:nil];\r\n\t\t\/**\r\n\t\t * Invio al delegate il messaggio che il download non &egrave; andato a buon fine.\r\n\t\t *\/\r\n\t\tif ([self.delegate respondsToSelector:@selector(autoDownload:didFailWithError:)])\r\n\t\t\t[delegate autoDownload:self didFailWithError:error];\r\n\t\t\r\n\t}\r\n\t[req release];\r\n}\r\n<\/pre>\n<p>Restano da esaminare i metodi dell NSURLConnectionDelegateProtocol.<\/p>\n<p>Questo metodo viene richiamato quando la connessione si avvia. Lo utilizzo per inizializzare il buffer dove memorizzare l&#8217;immagine:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response \r\n{\r\n    [receivedData setLength:0];\r\n}\r\n<\/pre>\n<p>Questo viene richiamato ogni qualvolta vengono ricevuti dei byte dalla connessione, mi preoccupo soltanto di accodarli a quelli gi\u00e0 ricevuti:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data \r\n{\r\n    [receivedData appendData:data];\r\n}\r\n<\/pre>\n<p>Questo metodo viene richiamato se la connessione termina con un codice d&#8217;errore, invoco quindi il metodo opportuno sul delegate per informarlo dell&#8217;accaduto:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n- (void)connection:(NSURLConnection *)connection\r\n  didFailWithError:(NSError *)error \r\n{\r\n    [connection release];\r\n\t\/**\r\n\t * Invio al delegate il messaggio che il download non &egrave; andato a buon fine.\r\n\t *\/\r\n    if ([delegate respondsToSelector:@selector(autoDownload:didFailWithError:)])\r\n        [delegate autoDownload:self didFailWithError:error];\r\n}\r\n<\/pre>\n<p>L&#8217;ultimo metodo viene richiamato quando la connection ha scaricato tutto il file. In questo caso sostituisco l&#8217;immagine <em>placeholder<\/em> con quella appena scaricata, verifico se \u00e8 necessario salvare il file ed eventualmente lo salvo. Infine avviso il delegate che tutto \u00e8 avvenuto secondo i piani.<br \/>\nIl delegate potrebbe ad esempio intercettare questa chiamata per eseguire un refresh dello schermo.<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\n- (void)connectionDidFinishLoading:(NSURLConnection *)connection \r\n{\r\n    \/**\r\n\t * Imposto la propriet&agrave; image con l'immagine scaricata da internet\r\n\t * questo spesso &egrave; sufficiente affinch&eacute; venga visualizzata\r\n\t * l'immagine corretta, se cos&igrave; non fosse il delegate dovr&agrave; invocare un setNeedDisplay\r\n\t *\/\r\n\tself.image = [UIImage imageWithData:receivedData];\r\n\t\r\n\t\/**\r\n\t * Salvo l'immagine nel filesystem se &egrave; stato specificato.\r\n\t *\/\r\n\tif (persistency) {\r\n\t\tNSString *documentsPath = \r\n\t\t\r\n\t\t[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];\r\n\t\t\r\n\t\tNSString *dbFullPath = [documentsPath stringByAppendingPathComponent:filename];\r\n\t\t[receivedData writeToFile:dbFullPath atomically:YES];\r\n\t}\r\n\t\/**\r\n\t * Invio al delegate il messaggio che il download &egrave; stato completato.\r\n\t *\/\r\n    if ([delegate respondsToSelector:@selector(autoDownloadImageViewFinishDownloading:)])\r\n        [delegate autoDownloadImageViewFinishDownloading:self];\r\n    \r\n    [connection release];\r\n    self.receivedData = nil;\r\n}\r\n<\/pre>\n<h4>Un esempio di utilizzo della nostra nuova classe<\/h4>\n<p>Spero che il codice sia abbastanza auto-esplicativo, un esempio di utilizzo alle volte vale pi\u00f9 di mille parole:<\/p>\n<pre lang=\"objc\" line=\"1\" escaped=\"true\">\r\nNSString *str = @\"http:\/\/www.brilliantstudent.in\/blog\/wp-content\/uploads\/2009\/03\/erdos1.jpg\";\r\n\tNSURL *url = [NSURL URLWithString:str];\r\n\tAutoDownloadImageView *img = [[AutoDownloadImageView alloc]\r\n\t\t\t\t\t\t\t\t  initWithFrame:CGRectMake(0, 0, 100, 100)\r\n\t\t\t\t\t\t\t\t  URL:url\r\n\t\t\t\t\t\t\t\t  persistency:YES\r\n\t\t\t\t\t\t\t\t  forceDownload:NO\r\n\t\t\t\t\t\t\t\t  ];\r\n\t[img setDelegate:self];\r\n\t[self.view addSubview:img];\r\n<\/pre>\n<p>Come potete notare le istanze della classe AutoDownloadImageView si usano come e dove si usano le UIImageview senza doversi preoccupare di altro, pi\u00f9 semplice di cos\u00ec!<\/p>\n<p>Basta aggiungere una istanza della classe AutoDownloadImageView dove vorremmo apparisse l&#8217;immagine e fornirle l&#8217;url dell&#8217;immagine da scaricare, tutto qui.<\/p>\n<p>Ogni suggerimento su come modificare la classe \u00e8 ben accetto, trovate il progetto su github a <a href=\"https:\/\/github.com\/ignazioc\/AutoDownloadImageView\" target=\"_blank\">questo indirizzo<\/a>.<\/p>\n<p>Buona programmazione!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Come avrete intuito dal titolo l&#8217;argomento di oggi \u00e8 il download asincrono delle immagini. Pi\u00f9 o meno&#8230;<\/p>\n","protected":false},"author":53,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[943,939,938,937,940,942,941,936],"class_list":["post-7793","post","type-post","status-publish","format-standard","hentry","category-tutorial-pratici","tag-download-immagini-xcode","tag-lazy-loading-ios","tag-nsoperationqueue","tag-nsthread","tag-nsurlconnection","tag-nsurlconnectiondelegateprotocol","tag-nsurlrequest","tag-thread-ios"],"acf":[],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/posts\/7793","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/users\/53"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/comments?post=7793"}],"version-history":[{"count":19,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/posts\/7793\/revisions"}],"predecessor-version":[{"id":7840,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/posts\/7793\/revisions\/7840"}],"wp:attachment":[{"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/media?parent=7793"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/categories?post=7793"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/tags?post=7793"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}