Handling download of image using SDWebImage while reusing UITableViewCell

前端 未结 3 805
眼角桃花
眼角桃花 2021-02-01 10:32

I have used third party library SDWebImage to download image for my UITableView cells, UIImageView is created within cell and fired request while configuring cell like this.

相关标签:
3条回答
  • 2021-02-01 11:18

    I met the same problem,I found UIImageView+WebCache cancel last download when a new download come.

    I not sure whether this is the intention of the author. So I write a new category of UIImageView base on SDWebImage.

    To view more: ImageDownloadGroup

    Easy to use:

    [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) {
    
                             }];
    
    0 讨论(0)
  • 2021-02-01 11:24

    The line that causes the download to get cancelled is in UIImageView+WebCache.m

    The first line in - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletedBlock)completedBlock

    calls [self cancelCurrentImageLoad];. If you get rid of this line, the download operations should continue on going.

    The problem at this point will be that there will be a race condition if a single UIImageView is waiting for multiple downloads to complete. The last download to complete may not necessarily be the last one started, so you may end up with the wrong image in your cell.

    There are a few ways to go about fixing this. The easiest might be just to add another associated object in UIImageView+WebCache, simply the last loaded URL. Then, you can check this URL in the completion block and only set the image if it matches.

    0 讨论(0)
  • 2021-02-01 11:25

    Effective iOS 10, the manual prefetching code of my original answer is no longer needed. Just set a prefetchDataSource. For example, in Swift 3:

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

    And then have a prefetchRowsAtIndexPaths which uses SDWebImagePrefetcher to fetch the rows

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

    And you can have the standard cellForRowAt:

    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
    }
    

    Personally, I prefer AlamofireImage. So the UITableViewDataSourcePrefetching is slightly different

    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)
        }
    }
    

    And obviously, the cellForRowAt would use 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
    }
    

    My original answer below, shows, for Objective-C, how you might do it in iOS versions before 10 (where we had to do our own prefetch calculations).


    This behavior, of canceling the download of cells that are no longer visible is precisely what keeps the asynchronous image retrieval so responsive when you scroll quickly, even with a slow Internet connection. For example, if you quickly scroll down to the 100th cell of the tableview, you really don't want to have that image retrieval to get backlogged behind the image retrieval for the preceding 99 rows (which are no longer visible). I'd suggest leaving the UIImageView category alone, but instead use SDWebImagePrefetcher if you want to prefetch images for cells that you're likely to scroll to.

    For example, where I call reloadData, I also prefetch the images for the ten cells immediately preceding and following the currently visible cells:

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

    Likewise, anytime I stop scrolling, I do the same:

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

    In terms of how I do that prefetch of the ten preceding and following cells, I do it like so:

    #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] == NSOrderedDescending)
                minimumIndexPath = indexPath;
            if ([maximumIndexPath compare:indexPath] == NSOrderedAscending)
                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 sharedImagePrefetcher] 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 (presumably 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 (presumably 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;
    }
    
    0 讨论(0)
提交回复
热议问题