2012-08-13 21 views
5

Sto utilizzando Grand Central Dispatch per caricare le immagini di un UITableViewCell in modo asincrono. Funziona bene, tranne che in alcuni casi di confine in cui la cella viene riutilizzata e un blocco precedente carica l'immagine sbagliata.Riutilizzo UITableViewCell con GCD

mio codice attuale è simile al seguente:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
    static NSString *CellIdentifier = @"Cell"; 

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 
    if (!cell) { 
     cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; 
    } 

    NSString *imagePath = [self imagePathForIndexPath:indexPath];   
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 

    dispatch_async(queue, ^{ 
     UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; 

     dispatch_sync(dispatch_get_main_queue(), ^{ 
      cell.imageView.image = image; 
      [cell setNeedsLayout]; 
     }); 
    }); 

    return cell; 
} 

Per quanto ne so code GCD non possono essere fermati. Come si può prevenire allora questo caso di confine? O dovrei usare qualcos'altro invece di GCD per risolvere questo problema?

+0

Sto usando GMGridView per mostrare una grande quantità di miniature di immagini e con lo stesso identico problema. Sto anche usando GCD per caricare le miniature dal mio webserver al volo. – Humayun

risposta

3

quello che ho fatto è stato aggiunto un ivar NSOperation alla cella. l'operazione è responsabile per ottenere, caricare e creare l'immagine. una volta completato, esegue il ping della cella. quando/se la cella viene rimossa dalla coda, l'operazione viene annullata e distrutta se non è stata completata. il test di funzionamento per la cancellazione quando in -main. dopo che l'immagine è passata alla cella, la cella lascia l'operazione e usa l'immagine.

+1

NSOperation è sicuramente migliore di GCD, di solito lo usa e metterlo all'interno della cella è sicuramente il modo migliore (ed elegante) ... +1 – meronix

+1

La sessione 211 del WWDC 2012 ha una buona panoramica di questo anche – jrturton

1

Si può provare a forzare un reset dell'immagine prima di avviare la richiesta asincrona.

Come utente scorrere la tabella, può vedere la vecchia immagine prima il metodo asincrono cambia con quella giusta

Basta aggiungere questa linea:

cell.imageView.image = nil; // or a placeHolder image 
dispatch_async(queue, ^{ 
    UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; 

    dispatch_sync(dispatch_get_main_queue(), ^{ 
     cell.imageView.image = image; 
     [cell setNeedsLayout]; 
    }); 
}); 

EDIT:

@hgpc:

Non penso che questo risolva il problema. Un blocco precedente può ancora impostare l'immagine sbagliata prima di un nuovo blocco. - HGPC 9 min fa

Hai ragione, se scrollare su e giù velocemente questo può essere un problema ...

l'unico modo che vedo è quello di fare una cella personalizzato con una proprietà bancone dove memorizzare (in un modo sync) il numero di operationn nella coda di ogni cella, e poi nel controllo di metodo asincrono che il contatore è == 1, prima di cambiare l'immagine:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 
     static NSString *CellIdentifier = @"Cell"; 

     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; 
     if (!cell) { 
    // MyCustomUITableViewCell has a property counter 
      cell = [[[MyCustomUITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; 
      cell.counter = 0; 
     } 

     NSString *imagePath = [self imagePathForIndexPath:indexPath];   
     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 

     cell.imageView.image = nil; // or a placeHolder image 
     cell.counter = cell.counter + 1; 
     dispatch_async(queue, ^{ 
      UIImage *image = [UIImage imageWithContentsOfFile:imagePath]; 

      dispatch_sync(dispatch_get_main_queue(), ^{ 
       if (cell.counter == 1){ 
        cell.imageView.image = image; 
        [cell setNeedsLayout]; 
       } 
       cell.counter = cell.counter - 1; 

      }); 
     }); 

    return cell; 
} 
+0

Non penso che questo risolva il problema. Un blocco precedente può ancora impostare l'immagine sbagliata prima di un nuovo blocco. – hpique

+0

right ... se la nuova modifica nella mia risposta – meronix

+0

Come ora, il contatore si assicura che sia impostata solo un'immagine, ma non necessariamente quella giusta. Sulla base del suggerimento di @ willy, penso che potresti semplicemente impostare il tag view con indexPath e controllare se è lo stesso prima di cambiare l'immagine. – hpique

0

è possibile impostare un identificatore di cella prima avviare l'attività asincrona e controllare che sia la stessa prima di aggiornare l'interfaccia utente.

+0

Perché il downvote? Questa sembra una buona idea. – hpique

0

Hai un paio di possibilità qui.

a) Utilizzare NSOperationQueue anziché GDC diretto.
NSOperation è basato su GDC e ha consentito un sacco di gestione come l'interruzione dei thread.
Dai un'occhiata allo concurrency guide di apple.

b) Utilizzare un caricatore di immagini asincrone pronto.
Ci sono abbastanza progetti gratuiti e open source disponibili su internetz.
Personalmente raccomando AFNetworking (in particolare la categoria AFNetworking+UIImageView) per quello.

Se vuoi farlo da solo (ricerca, conoscenza, ecc.) si dovrebbe stare con a), ma il metodo più conveniente è b).

Aggiornamento
ho appena notato, che si sta caricando l'immagine da un file e non dalla rete.
In questo caso, si dovrebbe gestire in modo diverso e prendersi cura presso i seguenti punti:

  • Evitare imageWithContentsOfFile: e piuttosto utilizzare imageNamed:. Questo perché imageNamed: memorizza nella cache le immagini una volta caricate e non lo fanno imageWithContentsOfFile:. Se è necessario utilizzare imageWithContentsOfFile:, precaricare tutte le immagini in un NSCache e popolare la tabella con immagini da questa cache.

  • Le immagini TableView non devono essere di grandi dimensioni (dimensione e file). Persino un'immagine retina di 100x100 px non ha più di un paio di kb che sicuramente non impiegheranno così tanto tempo per caricarsi da richiedere un carico asincrono.

+0

si noti che l'immagine viene caricata da un file, e questo è il ritardo che voglio rendere asincrona. Nessun download coinvolto. – hpique

+0

Ah capisco, si. Ma in questo caso hai altri problemi. Risolverò la mia risposta a breve. – yinkou

+0

Così ho aggiornato la mia risposta. – yinkou

0

Che dire di caricare solo le immagini quando la tabella non sta scorrendo? Il problema con cellForRowWithIndexPath: è che stai facendo un sacco di lavoro per le celle che stanno scorrendo.

Invece di caricare le immagini quando le celle vengono rimosse dalla coda, è possibile farlo su viewDidLoad (o ogni volta che il modello di dati viene inizializzato) e su scrollViewDidEndDecelerating:.

-(void)viewDidLoad { 
    [super viewDidLoad]; 
    [self initializeData]; 
    [self loadVisibleImages]; 
} 

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { 
    if (scrollView == self.tableView) { 
     [self loadVisibleImages]; 
    } 
} 

-(void)loadVisibleImages { 
    for (NSIndexPath *indexPath in [self.tableview indexPathsForVisibleRows]) { 
     dispatch_async(queue, ^{ 
      NSString *imagePath = [self imagePathForIndexPath:indexPath]; 
            UIImage *image = [UIImage imageWithContentsOfFile:imagePath];   
            dispatch_sync(dispatch_get_main_queue(), ^{ 
       UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; 
                cell.imageView.image = image; 
                [cell setNeedsLayout]; 
            }); 
        }); 
    } 
} 
Problemi correlati