Manejando la descarga de imágenes usando SDWebImage mientras reutiliza UITableViewCell

He utilizado una biblioteca de terceros SDWebImage para download la image de mis células UITableView, UIImageView se crea dentro de la célula y la request se ha activado mientras se configura una celda como esta.

[imageView setImageWithURL:[NSURL URLWithString:imageUrl] placeholderImage:[UIImage imageNamed:@"default.jpg"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) { }]; 

Funciona bien, sin embargo cuando me desploop rápido, la mayoría de las imágenes no se descargan por completo (lo puedo ver en charles) debido a que las imágenes no se almacenan en caching. ¿Cómo cacheo la request ya enviada a pesar de que mi celular se reutilizó, por lo que la misma request no se repetirá varias veces.

Por favor, ignore cualquier error tipográfico 🙂

Eficaz iOS 10, el código de búsqueda previa manual de mi respuesta original ya no es necesario. Simplemente establezca un prefetchDataSource . Por ejemplo, en Swift 3:

 override func viewDidLoad() { super.viewDidLoad() tableView.prefetchDataSource = self } 

Y luego tiene una prefetchRowsAtIndexPaths que utiliza SDWebImagePrefetcher para search las filas

 extension ViewController: UITableViewDataSourcePrefetching { public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let urls = indexPaths.map { baseURL.appendingPathComponent(images[$0.row]) } SDWebImagePrefetcher.shanetworking().prefetchURLs(urls) } } 

Y puede tener el cellForRowAt estándar:

 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let url = baseURL.appendingPathComponent(images[indexPath.row]) cell.imageView?.sd_setImage(with: url, placeholderImage: placeholder) return cell } 

Personalmente, prefiero AlamofireImage. Entonces, el UITableViewDataSourcePrefetching es ligeramente diferente

 extension ViewController: UITableViewDataSourcePrefetching { public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let requests = indexPaths.map { URLRequest(url: baseURL.appendingPathComponent(images[$0.row])) } AlamofireImage.ImageDownloader.default.download(requests) } } 

Y obviamente, cellForRowAt usaría af_setImage :

 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let url = baseURL.appendingPathComponent(images[indexPath.row]) cell.imageView?.af_setImage(withURL: url, placeholderImage: placeholder) return cell } 

Mi respuesta original a continuación muestra, para Objective-C, cómo puede hacerlo en versiones de iOS antes de 10 (donde tuvimos que hacer nuestros propios cálculos de captación previa).


Este comportamiento, de cancelar la descarga de celdas que ya no son visibles, es precisamente lo que hace que la recuperación de imágenes asíncronas sea tan receptiva cuando se desplaza rápidamente, incluso con una connection lenta a Internet. Por ejemplo, si rápidamente se desplaza hacia abajo hasta la celda 100 de la vista de tabla, realmente no desea que esa recuperación de imágenes se atrase detrás de la recuperación de imágenes para las 99 filas anteriores (que ya no son visibles). Sugiero dejar la categoría de UIImageView solo, pero en su lugar use SDWebImagePrefetcher si desea SDWebImagePrefetcher imágenes para las celdas con las que probablemente se desplazará.

Por ejemplo, donde reloadData a reloadData , también presento las imágenes previas para las diez celdas inmediatamente anteriores y siguientes a las celdas actualmente visibles:

 [self.tableView reloadData]; dispatch_async(dispatch_get_main_queue(), ^{ [self prefetchImagesForTableView:self.tableView]; }); 

Del mismo modo, en cualquier momento que deje de desplazarme, hago lo mismo:

 - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self prefetchImagesForTableView:self.tableView]; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (!decelerate) [self prefetchImagesForTableView:self.tableView]; } 

En términos de cómo hago esa búsqueda previa de las diez celdas anteriores y siguientes, lo hago así:

 #pragma mark - Prefetch cells static NSInteger const kPrefetchRowCount = 10; /** Prefetch a certain number of images for rows prior to and subsequent to the currently visible cells * * @param tableView The tableview for which we're going to prefetch images. */ - (void)prefetchImagesForTableView:(UITableView *)tableView { NSArray *indexPaths = [self.tableView indexPathsForVisibleRows]; if ([indexPaths count] == 0) return; NSIndexPath *minimumIndexPath = indexPaths[0]; NSIndexPath *maximumIndexPath = [indexPaths lastObject]; // they should be sorted already, but if not, update min and max accordingly for (NSIndexPath *indexPath in indexPaths) { if ([minimumIndexPath compare:indexPath] == NSOrdenetworkingDescending) minimumIndexPath = indexPath; if ([maximumIndexPath compare:indexPath] == NSOrdenetworkingAscending) maximumIndexPath = indexPath; } // build array of imageURLs for cells to prefetch NSMutableArray<NSIndexPath *> *prefetchIndexPaths = [NSMutableArray array]; NSArray<NSIndexPath *> *precedingRows = [self tableView:tableView indexPathsForPrecedingRows:kPrefetchRowCount fromIndexPath:minimumIndexPath]; [prefetchIndexPaths addObjectsFromArray:precedingRows]; NSArray<NSIndexPath *> *followingRows = [self tableView:tableView indexPathsForFollowingRows:kPrefetchRowCount fromIndexPath:maximumIndexPath]; [prefetchIndexPaths addObjectsFromArray:followingRows]; // build array of imageURLs for cells to prefetch (how you get the image URLs will vary based upon your implementation) NSMutableArray<NSURL *> *urls = [NSMutableArray array]; for (NSIndexPath *indexPath in prefetchIndexPaths) { NSURL *url = self.objects[indexPath.row].imageURL; if (url) { [urls addObject:url]; } } // now prefetch if ([urls count] > 0) { [[SDWebImagePrefetcher shanetworkingImagePrefetcher] prefetchURLs:urls]; } } /** Retrieve NSIndexPath for a certain number of rows preceding particular NSIndexPath in the table view. * * @param tableView The tableview for which we're going to retrieve indexPaths. * @param count The number of rows to retrieve * @param indexPath The indexPath where we're going to start (presumbly the first visible indexPath) * * @return An array of indexPaths. */ - (NSArray<NSIndexPath *> *)tableView:(UITableView *)tableView indexPathsForPrecedingRows:(NSInteger)count fromIndexPath:(NSIndexPath *)indexPath { NSMutableArray *indexPaths = [NSMutableArray array]; NSInteger row = indexPath.row; NSInteger section = indexPath.section; for (NSInteger i = 0; i < count; i++) { if (row == 0) { if (section == 0) { return indexPaths; } else { section--; row = [tableView numberOfRowsInSection:section] - 1; } } else { row--; } [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]]; } return indexPaths; } /** Retrieve NSIndexPath for a certain number of following particular NSIndexPath in the table view. * * @param tableView The tableview for which we're going to retrieve indexPaths. * @param count The number of rows to retrieve * @param indexPath The indexPath where we're going to start (presumbly the last visible indexPath) * * @return An array of indexPaths. */ - (NSArray<NSIndexPath *> *)tableView:(UITableView *)tableView indexPathsForFollowingRows:(NSInteger)count fromIndexPath:(NSIndexPath *)indexPath { NSMutableArray *indexPaths = [NSMutableArray array]; NSInteger row = indexPath.row; NSInteger section = indexPath.section; NSInteger rowCountForSection = [tableView numberOfRowsInSection:section]; for (NSInteger i = 0; i < count; i++) { row++; if (row == rowCountForSection) { row = 0; section++; if (section == [tableView numberOfSections]) { return indexPaths; } rowCountForSection = [tableView numberOfRowsInSection:section]; } [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]]; } return indexPaths; } 

La línea que hace que la descarga se cancele está en UIImageView+WebCache.m

La primera línea en - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletedBlock)completedBlock

llamadas [self cancelCurrentImageLoad]; . Si se deshace de esta línea, las operaciones de descarga deben continuar.

El problema en este momento será que habrá una condición de carrera si un único UIImageView está esperando que se completen varias descargas. La última descarga para completar puede no ser necesariamente la última que se inició, por lo que puede terminar con una image incorrecta en su celular.

Hay algunas maneras de solucionar este problema. Lo más fácil es simplemente agregar otro object asociado en UIImageView+WebCache , simplemente la última URL cargada. Luego, puede verificar esta URL en el bloque de finalización y solo establecer la image si coincide.

Encontré el mismo problema, encontré UIImageView + WebCache cancelar la última descarga cuando se produjo una nueva descarga.

No estoy seguro de si esta es la intención del autor. Entonces escribo una nueva category de UIImageView base en SDWebImage.

Para ver más: ImageDownloadGroup

Fácil de usar:

 [cell.imageView mq_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] groupIdentifier:@"customGroupID" completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) { }];