Carga de imagen asíncrona desde url dentro de una celda UITableView: la imagen cambia a una imagen incorrecta mientras se desplaza

He escrito dos formas de sincronizar las imágenes de carga dentro de mi celda UITableView. En ambos casos, la imagen se cargará bien, pero cuando vaya a desplazar la tabla, las imágenes cambiarán varias veces hasta que el desplazamiento termine y la imagen regrese a la imagen de la derecha. No tengo idea de por qué está sucediendo esto.

#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) - (void)viewDidLoad { [super viewDidLoad]; dispatch_async(kBgQueue, ^{ NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString: @"http://myurl.com/getMovies.php"]]; [self performSelectorOnMainThread:@selector(fetchedData:) withObject:data waitUntilDone:YES]; }); } -(void)fetchedData:(NSData *)data { NSError* error; myJson = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; [_myTableView reloadData]; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ // Return the number of rows in the section. // Usually the number of items in your array (the one that holds your list) NSLog(@"myJson count: %d",[myJson count]); return [myJson count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (cell == nil) { cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } dispatch_async(kBgQueue, ^{ NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://sofes.miximages.com/ios/f",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]]; dispatch_async(dispatch_get_main_queue(), ^{ cell.poster.image = [UIImage imageWithData:imgData]; }); }); return cell; } 

… …

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (cell == nil) { cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://sofes.miximages.com/ios/f",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]; NSURLRequest* request = [NSURLRequest requestWithURL:url]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * response, NSData * data, NSError * error) { if (!error){ cell.poster.image = [UIImage imageWithData:data]; // do whatever you want with image } }]; return cell; } 

Suponiendo que está buscando una solución táctica rápida, lo que debe hacer es asegurarse de que la imagen de la celda esté inicializada y que la fila de la celda aún esté visible, por ejemplo:

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"]; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://sofes.miximages.com/ios/f", self.myJson[indexPath.row][@"movieId"]]]; NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (data) { UIImage *image = [UIImage imageWithData:data]; if (image) { dispatch_async(dispatch_get_main_queue(), ^{ MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath]; if (updateCell) updateCell.poster.image = image; }); } } }]; [task resume]; return cell; } 

El código anterior resuelve algunos problemas derivados del hecho de que la celda se reutiliza:

  1. No está inicializando la imagen de la celda antes de iniciar la solicitud de fondo (lo que significa que la última imagen de la celda eliminada seguirá siendo visible mientras se descarga la nueva imagen). Asegúrate de nil la propiedad de la image de cualquier vista de imagen o de lo contrario verás el parpadeo de las imágenes.

  2. Un problema más sutil es que en una red realmente lenta, su solicitud asincrónica podría no finalizar antes de que la celda se desplace fuera de la pantalla. Puede usar el método cellForRowAtIndexPath: (no debe confundirse con el método similar llamado UITableViewDataSource tableView:cellForRowAtIndexPath: para ver si la celda de esa fila aún es visible. Este método devolverá nil si la celda no es visible.

    El problema es que la celda se ha desplazado cuando se completó el método asíncrono y, lo que es peor, la celda se reutilizó para otra fila de la tabla. Al verificar si la fila aún está visible, se asegurará de que no actualice la imagen accidentalmente con la imagen de una fila que se haya desplazado desde la pantalla.

  3. Poco relacionado con la cuestión que nos ocupa, todavía me sentía obligado a actualizar esto para aprovechar las convenciones y API modernas, en particular:

    • Use NSURLSession lugar de despachar -[NSData contentsOfURL:] a una cola en segundo plano;

    • Utilice dequeueReusableCellWithIdentifier:forIndexPath: lugar de dequeueReusableCellWithIdentifier: (pero asegúrese de usar prototipo de celda o clase de registro o NIB para ese identificador); y

    • Usé un nombre de clase que se ajusta a las convenciones de nomenclatura de Cocoa (es decir, empiezo con la letra mayúscula).

Incluso con estas correcciones, hay problemas:

  1. El código anterior no almacena en caché las imágenes descargadas. Eso significa que si desplaza una imagen fuera de pantalla y vuelve a la pantalla, la aplicación puede intentar recuperar la imagen nuevamente. Quizás tenga la suerte de que los encabezados de respuesta del servidor permitan el almacenamiento en caché bastante transparente ofrecido por NSURLSession y NSURLCache , pero de lo contrario, realizará solicitudes de servidor innecesarias y ofrecerá un UX mucho más lento.

  2. No estamos cancelando solicitudes de celdas que se desplazan fuera de la pantalla. Por lo tanto, si se desplaza rápidamente a la fila 100, la imagen de esa fila podría acumularse detrás de las solicitudes de las 99 filas anteriores que ya no son visibles. Siempre quiere asegurarse de priorizar las solicitudes de celdas visibles para obtener el mejor UX.

La solución más sencilla que soluciona estos problemas es utilizar una categoría UIImageView , como la proporcionada con SDWebImage o AFNetworking . Si lo desea, puede escribir su propio código para tratar los problemas anteriores, pero es mucho trabajo, y las categorías anteriores de UIImageView ya lo han hecho por usted.

/ * Lo hice de esta manera, y también lo probé * /

Paso 1 = Registrar la clase de celda personalizada (en el caso de una celda prototipo en la tabla) o una punta (en el caso de la punta personalizada para la celda personalizada) para una tabla como esta en el método viewDidLoad:

 [self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"]; 

O

 [self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"]; 

Paso 2 = Use el método “dequeueReusableCellWithIdentifier: forIndexPath:” de UITableView como este (para esto, debe registrar clase o punta):

  - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell" forIndexPath:indexPath]; cell.imageViewCustom.image = nil; // [UIImage imageNamed:@"default.png"]; cell.textLabelCustom.text = @"Hello"; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // retrive image on global queue UIImage * img = [UIImage imageWithData:[NSData dataWithContentsOfURL: [NSURL URLWithString:kImgLink]]]; dispatch_async(dispatch_get_main_queue(), ^{ CustomTableViewCell * cell = (CustomTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]; // assign cell image on main thread cell.imageViewCustom.image = img; }); }); return cell; } 

Existen marcos múltiples que resuelven este problema. Sólo para nombrar unos pocos:

Rápido:

  • Nuke (mío)
  • Martín pescador
  • AlamofireImage
  • HanekeSwift

C objective:

  • AFNetworking
  • PINRemoteImage
  • YYWebImage
  • SDWebImage

Swift 3

Escribo mi propia implementación ligera para el cargador de imágenes con el uso de NSCache. ¡No parpadea la imagen de la célula!

ImageCacheLoader.swift

 typealias ImageCacheLoaderCompletionHandler = ((UIImage) -> ()) class ImageCacheLoader { var task: URLSessionDownloadTask! var session: URLSession! var cache: NSCache! init() { session = URLSession.shared task = URLSessionDownloadTask() self.cache = NSCache() } func obtainImageWithPath(imagePath: String, completionHandler: @escaping ImageCacheLoaderCompletionHandler) { if let image = self.cache.object(forKey: imagePath as NSString) { DispatchQueue.main.async { completionHandler(image) } } else { /* You need placeholder image in your assets, if you want to display a placeholder to user */ let placeholder = #imageLiteral(resourceName: "placeholder") DispatchQueue.main.async { completionHandler(placeholder) } let url: URL! = URL(string: imagePath) task = session.downloadTask(with: url, completionHandler: { (location, response, error) in if let data = try? Data(contentsOf: url) { let img: UIImage! = UIImage(data: data) self.cache.setObject(img, forKey: imagePath as NSString) DispatchQueue.main.async { completionHandler(img) } } }) task.resume() } } } 

Ejemplo de uso

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier") cell.title = "Cool title" imageLoader.obtainImageWithPath(imagePath: viewModel.image) { (image) in // Before assigning the image, check whether the current cell is visible if let updateCell = tableView.cellForRow(at: indexPath) { updateCell.imageView.image = image } } return cell } 

La mejor respuesta no es la forma correcta de hacerlo :(. En realidad vinculó indexPath con el modelo, lo que no siempre es bueno. Imagine que se han agregado algunas filas durante la carga de la imagen. Ahora la celda para indexPath dado existe en la pantalla, pero la imagen ya no es correcto! La situación es poco probable y difícil de replicar, pero es posible.

Es mejor utilizar el enfoque MVVM, vincular la celda con viewModel en el controlador y cargar la imagen en viewModel (asignando la señal ReactiveCocoa con el método switchToLatest), luego suscribir esta señal y asignarle una imagen a la celda. 😉

Debes recordar no abusar de MVVM. ¡Las vistas tienen que ser muy simples! ¡Mientras que ViewModels debería ser reutilizable! Es por eso que es muy importante vincular View (UITableViewCell) y ViewModel en el controlador.

En mi caso, no fue debido al almacenamiento en caché de la imagen (SDWebImage usado). Fue debido a la incompatibilidad de tags de la celda personalizada con indexPath.row.

En cellForRowAtIndexPath:

1) Asigne un valor de índice a su celda personalizada. Por ejemplo,

 cell.tag = indexPath.row 

2) En el hilo principal, antes de asignar la imagen, compruebe si la imagen pertenece a la celda correspondiente haciendo coincidirla con la etiqueta.

 dispatch_async(dispatch_get_main_queue(), ^{ if(cell.tag == indexPath.row) { UIImage *tmpImage = [[UIImage alloc] initWithData:imgData]; thumbnailImageView.image = tmpImage; }}); }); 

Aquí está la versión rápida (usando el código objective C de @Nitesh Borad):

  if let img: UIImage = UIImage(data: previewImg[indexPath.row]) { cell.cardPreview.image = img } else { // The image isn't cached, download the img data // We should perform this in a background thread let imgURL = NSURL(string: "webLink URL") let request: NSURLRequest = NSURLRequest(URL: imgURL!) let session = NSURLSession.sharedSession() let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in let error = error let data = data if error == nil { // Convert the downloaded data in to a UIImage object let image = UIImage(data: data!) // Store the image in to our cache self.previewImg[indexPath.row] = data! // Update the cell dispatch_async(dispatch_get_main_queue(), { if let cell: YourTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? YourTableViewCell { cell.cardPreview.image = image } }) } else { cell.cardPreview.image = UIImage(named: "defaultImage") } }) task.resume() } 
  - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"]; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://sofes.miximages.com/ios/f", self.myJson[indexPath.row][@"movieId"]]]; NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (data) { UIImage *image = [UIImage imageWithData:data]; if (image) { dispatch_async(dispatch_get_main_queue(), ^{ MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath]; if (updateCell) updateCell.poster.image = image; }); } } }]; [task resume]; return cell; } 

Gracias “Rob” …. Tuve el mismo problema con UICollectionView y su respuesta me ayudó a resolver mi problema. Aquí está mi código:

  if ([Dict valueForKey:@"ImageURL"] != [NSNull null]) { cell.coverImageView.image = nil; cell.coverImageView.imageURL=nil; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if ([Dict valueForKey:@"ImageURL"] != [NSNull null] ) { dispatch_async(dispatch_get_main_queue(), ^{ myCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath]; if (updateCell) { cell.coverImageView.image = nil; cell.coverImageView.imageURL=nil; cell.coverImageView.imageURL=[NSURL URLWithString:[Dict valueForKey:@"ImageURL"]]; } else { cell.coverImageView.image = nil; cell.coverImageView.imageURL=nil; } }); } }); } else { cell.coverImageView.image=[UIImage imageNamed:@"default_cover.png"]; } 

Creo que desea acelerar la carga de su celda en el momento de cargar la imagen para la celda en el fondo. Para eso hemos hecho los siguientes pasos:

  1. Verificando que el archivo existe en el directorio del documento o no.

  2. Si no, carga la imagen por primera vez y guárdala en nuestro directorio de documentos del teléfono. Si no desea guardar la imagen en el teléfono, puede cargar las imágenes de la celda directamente en el fondo.

  3. Ahora el proceso de carga:

Solo incluya: #import "ManabImageOperations.h"

El código es el siguiente para una celda:

 NSString *imagestr=[NSString stringWithFormat:@"http://www.yourlink.com/%@",[dictn objectForKey:@"member_image"]]; NSString *docDir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]; NSLog(@"Doc Dir: %@",docDir); NSString *pngFilePath = [NSString stringWithFormat:@"%@/%@",docDir,[dictn objectForKey:@"member_image"]]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pngFilePath]; if (fileExists) { [cell1.memberimage setImage:[UIImage imageWithContentsOfFile:pngFilePath] forState:UIControlStateNormal]; } else { [ManabImageOperations processImageDataWithURLString:imagestr andBlock:^(NSData *imageData) { [cell1.memberimage setImage:[[UIImage alloc]initWithData: imageData] forState:UIControlStateNormal]; [imageData writeToFile:pngFilePath atomically:YES]; }]; } 

ManabImageOperations.h:

 #import  @interface ManabImageOperations : NSObject { } + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage; @end 

ManabImageOperations.m:

 #import "ManabImageOperations.h" #import  @implementation ManabImageOperations + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage { NSURL *url = [NSURL URLWithString:urlString]; dispatch_queue_t callerQueue = dispatch_get_main_queue(); dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL); dispatch_async(downloadQueue, ^{ NSData * imageData = [NSData dataWithContentsOfURL:url]; dispatch_async(callerQueue, ^{ processImage(imageData); }); }); // downloadQueue=nil; dispatch_release(downloadQueue); } @end 

Por favor, compruebe la respuesta y comente si se produce algún problema …

Simplemente cambia,

 dispatch_async(kBgQueue, ^{ NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString: [NSString stringWithFormat:@"http://sofes.miximages.com/ios/f",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]]; dispatch_async(dispatch_get_main_queue(), ^{ cell.poster.image = [UIImage imageWithData:imgData]; }); }); 

Dentro

  dispatch_async(kBgQueue, ^{ NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString: [NSString stringWithFormat:@"http://sofes.miximages.com/ios/f",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]]; cell.poster.image = [UIImage imageWithData:imgData]; dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; }); }); 

Puedes pasar tu URL,

 NSURL *url = [NSURL URLWithString:@"http://sofes.miximages.com/ios/f"]; NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (data) { UIImage *image = [UIImage imageWithData:data]; if (image) { dispatch_async(dispatch_get_main_queue(), ^{ yourimageview.image = image; }); } } }]; [task resume]; 
 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ Static NSString *CellIdentifier = @"Cell"; QTStaffViewCell *cell = (QTStaffViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; If (cell == nil) { NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QTStaffViewCell" owner:self options:nil]; cell = [nib objectAtIndex: 0]; } StaffData = [self.staffArray objectAtIndex:indexPath.row]; NSString *title = StaffData.title; NSString *fName = StaffData.firstname; NSString *lName = StaffData.lastname; UIFont *FedSanDemi = [UIFont fontWithName:@"Aller" size:18]; cell.drName.text = [NSString stringWithFormat:@"%@ %@ %@", title,fName,lName]; [cell.drName setFont:FedSanDemi]; UIFont *aller = [UIFont fontWithName:@"Aller" size:14]; cell.drJob.text = StaffData.job; [cell.drJob setFont:aller]; if ([StaffData.title isEqualToString:@"Dr"]) { cell.drJob.frame = CGRectMake(83, 26, 227, 40); } else { cell.drJob.frame = CGRectMake(90, 26, 227, 40); } if ([StaffData.staffPhoto isKindOfClass:[NSString class]]) { NSURL *url = [NSURL URLWithString:StaffData.staffPhoto]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url completionHandler:^(NSURL *location,NSURLResponse *response, NSError *error) { NSData *imageData = [NSData dataWithContentsOfURL:location]; UIImage *image = [UIImage imageWithData:imageData]; dispatch_sync(dispatch_get_main_queue(), ^{ cell.imageView.image = image; }); }]; [task resume]; } return cell;}