Descarga asíncrona de imágenes para UITableView con GCD

Estoy descargando imágenes para mi uitableview de forma asíncrona usando GCD, pero hay un problema: cuando las imágenes de desplazamiento parpadean y cambian todo el time. Intenté configurar la image a cero con cada celda, pero no ayuda mucho. Cuando retrocede rápidamente, todas las imágenes están mal. ¿Qué puedo hacer al respecto? Aquí está mi método para celdas:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; if (self.loader.parsedData[indexPath.row] != nil) { cell.imageView.image = nil; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); dispatch_async(queue, ^(void) { NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]]; UIImage* image = [[UIImage alloc] initWithData:imageData]; dispatch_async(dispatch_get_main_queue(), ^{ cell.imageView.image = image; [cell setNeedsLayout]; }); }); cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"]; } return cell; } 

El problema aquí es que tus bloques de búsqueda de imágenes contienen references a las celdas de tableview. Cuando se completa la descarga, establece la propiedad imageView.image , incluso si ha reciclado la celda para mostrar una fila diferente.

Necesitará su bloque de finalización de descarga para probar si la image sigue siendo relevante para la célula antes de configurar la image.

También vale la pena señalar que no está almacenando las imágenes en ninguna otra parte que no sea la celda, por lo que las downloadá nuevamente cada vez que desplace una fila en la pantalla. Probablemente quiera almacenarlos en caching en alguna parte y search imágenes almacenadas en caching local antes de comenzar una descarga.

Editar: aquí hay una manera simple de probar, usando la propiedad de tag la celda:

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; cell.tag = indexPath.row; NSDictionary *parsedData = self.loader.parsedData[indexPath.row]; if (parsedData) { cell.imageView.image = nil; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); dispatch_async(queue, ^(void) { NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:parsedData[@"imageLR"]]; UIImage* image = [[UIImage alloc] initWithData:imageData]; if (image) { dispatch_async(dispatch_get_main_queue(), ^{ if (cell.tag == indexPath.row) { cell.imageView.image = image; [cell setNeedsLayout]; } }); } }); cell.textLabel.text = parsedData[@"id"]; } return cell; } 

El punto es que no comprendiste completamente el concepto de reutilización de células. Eso no constring muy bien con las descargas asíncronas.

El bloque

  ^{ cell.imageView.image = image; [cell setNeedsLayout]; } 

se ejecuta cuando finaliza la request y se cargan todos los datos. Pero la celda obtiene su valor cuando se crea el bloque.

Para cuando se ejecuta el bloque, la celda apunta a una de las celdas existentes. Pero es bastante probable que el usuario continúe desplazándose. El object de la celda se reutilizó mientras tanto y la image está asociada con una celda ' antigua ' que se reutiliza, asigna y muestra. Poco después, la image correcta se carga, se asigna y se muestra a less que el usuario haya seguido desplazándose. Y así sucesivamente y así sucesivamente.

Deberías search una manera más inteligente de hacerlo. Hay muchos turorials. Google para la carga de imágenes perezosas.

Usa la ruta del índice para get la celda. Si no está visible, la celda será nil y no tendrá problemas. Por supuesto, es probable que desee almacenar en caching los datos cuando se descarguen para configurar la image de la celda inmediatamente cuando ya tiene la image.

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; if (self.loader.parsedData[indexPath.row] != nil) { cell.imageView.image = nil; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); dispatch_async(queue, ^(void) { // You may want to cache this explicitly instead of reloading every time. NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]]; UIImage* image = [[UIImage alloc] initWithData:imageData]; dispatch_async(dispatch_get_main_queue(), ^{ // Capture the indexPath variable, not the cell variable, and use that UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath]; blockCell.imageView.image = image; [blockCell setNeedsLayout]; }); }); cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"]; } return cell; } 

He estado estudiando sobre este problema y encontré un enfoque excelente al personalizar un UITableViewCell.

 #import <UIKit/UIKit.h> @interface MyCustomCell : UITableViewCell @property (nonatomic, strong) NSURLSessionDataTask *imageDownloadTask; @property (nonatomic, weak) IBOutlet UIImageView *myImageView; @property (nonatomic, weak) IBOutlet UIActivityIndicatorView *activityIndicator; @end 

Ahora, en su TableViewController, declare dos properties para NSURLSessionConfiguration y NSURLSession e inicielas en ViewDidLoad:

 @interface MyTableViewController () @property (nonatomic, strong) NSURLSessionConfiguration *sessionConfig; @property (nonatomic, strong) NSURLSession *session; . . . @end @implementation TimesVC - (void)viewDidLoad { [super viewDidLoad]; _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; _session = [NSURLSession sessionWithConfiguration:_sessionConfig]; } . . . 

Supongamos que su fuente de datos es una matriz de NSMutableDictionary (o NSManagedObjectContext). Puede download fácilmente la image para cada celda, con el almacenamiento en caching, así:

 . . . - (MyCustomCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCustomCell *cell = (MyCustomCell *)[tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; if (!cell) { cell = [[MyCustomCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } NSMutableDictionary *myDictionary = [_myArrayDataSource objectAtIndex:indexPath.row]; if (cell.imageDownloadTask) { [cell.imageDownloadTask cancel]; } [cell.activityIndicator startAnimating]; cell.myImageView.image = nil; if (![myDictionary valueForKey:@"image"]) { NSString *urlString = [myDictionary valueForKey:@"imageURL"]; NSURL *imageURL = [NSURL URLWithString:urlString]; if (imageURL) { cell.imageDownloadTask = [_session dataTaskWithURL:imageURL completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { if (error) { NSLog(@"ERROR: %@", error); } else { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; if (httpResponse.statusCode == 200) { UIImage *image = [UIImage imageWithData:data]; dispatch_async(dispatch_get_main_queue(), ^{ [myDictionary setValue:data forKey:@"image"]; [cell.myImageView setImage:image]; [cell.activityIndicator stopAnimating]; }); } else { NSLog(@"Couldn't load image at URL: %@", imageURL); NSLog(@"HTTP %d", httpResponse.statusCode); } } }]; [cell.imageDownloadTask resume]; } } else { [cell.myImageView setImage:[UIImage imageWithData:[myDictionary valueForKey:@"image"]]]; [cell.activityIndicator stopAnimating]; } return cell; } 

¡Espero que ayude a algunos desarrolladores! Aclamaciones.

CRÉDITOS: Vista de tabla de imágenes en iOS 7

No reinvente la rueda, solo use este par de pods increíbles:

https://github.com/rs/SDWebImage

https://github.com/JJSaccolo/UIActivityIndicator-for-SDWebImage

Tan simple como:

  [self.eventImage setImageWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", [SchemeConfiguration APIEndpoint] , _event.imageUrl]] placeholderImage:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { event.image = UIImagePNGRepresentation(image); } usingActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; 

¿Has pensado en usar SDWebImage? Descarga la image de la URL dada también de forma asíncrona. No utilicé toda la biblioteca (importó solo UIImageView + WebCache.h). Una vez importado, todo lo que tiene que hacer es llamar al método como tal:

 [UIImageXYZ sd_setImageWithURL:["url you're retrieving from"] placeholderImage:[UIImage imageNamed:@"defaultProfile.jpeg"]]; 

Podría ser una exageración si estás usando AFNetworking 2.0, pero funcionó para mí.

Aquí está el enlace al github si quieres probarlo.

Además de la respuesta aceptada de Seamus Campbell , también debes saber que en algún momento esto no funciona. En ese caso, deberíamos volver a cargar la celda específica. Asi que

 if (image) { dispatch_async(dispatch_get_main_queue(), ^{ if (cell.tag == indexPath.row) { cell.imageView.image = image; [cell setNeedsLayout]; } }); } 

debe ser cambiado a,

  if (image) { dispatch_async(dispatch_get_main_queue(), ^{ if (cell.tag == indexPath.row) { cell.imageView.image = image; self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None) } }); }