Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SDWebImage does not load remote images until scroll

I am using SDWebImage library to load remote images into a table view which uses a custom cell class i have created. I simply use

[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"loading.jpg"]];

in cellForRowAtIndexPath: Now the problem is it loads images in the visible cells only and not for cells that are offscreen for which i have to scroll up and down to make them load. Is there any way i can load all images without having to scroll the table view. Thanks in advance!!

like image 687
user2082760 Avatar asked Mar 29 '13 10:03

user2082760


2 Answers

If you want to prefetch rows, you can respond to UIScrollViewDelegate methods to determine when the table scrolling is done, triggering a prefetch of the rows. You can perform the prefetch using SDWebImagePrefetcher (in my original answer I was a little dismissive of this useful class, but it seems to work relatively well now):

- (void)viewDidLoad
{
    [super viewDidLoad];

    // the details don't really matter here, but the idea is to fetch data, 
    // call `reloadData`, and then prefetch the other images

    NSURL *url = [NSURL URLWithString:kUrlWithJSONData];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (connectionError) {
            NSLog(@"sendAsynchronousRequest error: %@", connectionError);
            return;
        }

        self.objects = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

        [self.tableView reloadData];

        [self prefetchImagesForTableView:self.tableView];
    }];
}

// some of the basic `UITableViewDataDelegate` methods have been omitted because they're not really relevant

Here is the simple cellForRowAtIndexPath (not entirely relevant, but just showing that if you use SDWebImagePrefetcher, you don't have to mess around with cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"Cell";
    CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    NSAssert([cell isKindOfClass:[CustomCell class]], @"cell should be CustomCell");

    [cell.customImageView setImageWithURL:[self urlForIndexPath:indexPath] placeholderImage:nil];
    [cell.customLabel setText:[self textForIndexPath:indexPath]];

    return cell;
}

These UIScrollViewDelegate methods prefetch more rows when scrolling finishes

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    // if `decelerate` was true for `scrollViewDidEndDragging:willDecelerate:`
    // this will be called when the deceleration is done

    [self prefetchImagesForTableView:self.tableView];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    // if `decelerate` is true, then we shouldn't start prefetching yet, because
    // `cellForRowAtIndexPath` will be hard at work returning cells for the currently visible
    // cells.

    if (!decelerate)
        [self prefetchImagesForTableView:self.tableView];
}

You obviously need to implement a prefetch routine. This gets the NSIndexPath values for the cells on each side of the visible cells, gets their image URLs, and then prefetches that data.

/** 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 (indexPath.section < minimumIndexPath.section || (indexPath.section == minimumIndexPath.section && indexPath.row < minimumIndexPath.row)) minimumIndexPath = indexPath;
        if (indexPath.section > maximumIndexPath.section || (indexPath.section == maximumIndexPath.section && indexPath.row > maximumIndexPath.row)) maximumIndexPath = indexPath;
    }

    // build array of imageURLs for cells to prefetch

    NSMutableArray *imageURLs = [NSMutableArray array];
    indexPaths = [self tableView:tableView priorIndexPathCount:kPrefetchRowCount fromIndexPath:minimumIndexPath];
    for (NSIndexPath *indexPath in indexPaths)
        [imageURLs addObject:[self urlForIndexPath:indexPath]];
    indexPaths = [self tableView:tableView nextIndexPathCount:kPrefetchRowCount fromIndexPath:maximumIndexPath];
    for (NSIndexPath *indexPath in indexPaths)
        [imageURLs addObject:[self urlForIndexPath:indexPath]];

    // now prefetch

    if ([imageURLs count] > 0)
    {
        [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:imageURLs];
    }
}

These are the utility methods for getting the NSIndexPath for the rows immediately preceding the visible cells as well as those immediately following the visible cells:

/** 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 *)tableView:(UITableView *)tableView priorIndexPathCount:(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 *)tableView:(UITableView *)tableView nextIndexPathCount:(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;
}

There's a lot there, but in reality, SDWebImage and its SDWebImagePrefetcher is doing the heavy lifting.

I include my original answer below for the sake of completeness.


Original answer:

If you want to do some prefetching with SDWebImage, you could do something like the following:

  1. Add a completion block to your setImageWithURL call:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        NSLog(@"%s", __FUNCTION__);
    
        static NSString *cellIdentifier = @"Cell";
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    
        TableModelRow *rowData = self.objects[indexPath.row];
    
        cell.textLabel.text = rowData.title;
        [cell.imageView setImageWithURL:rowData.url
                       placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                              completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
                                  [self prefetchImagesForTableView:tableView];
                              }];
    
        return cell;
    }
    

    I must confess I don't really like calling my prefetcher routine here (I wish iOS had some nice didFinishTableRefresh delegate method), but it works, even if it's calling the routine more times than I'd really want. I just make sure below that the routine below makes sure that it won't make redundant requests.

  2. Anyway, I write a prefetch routine that looks for, say, the next ten images:

    const NSInteger kPrefetchRowCount = 10;
    
    - (void)prefetchImagesForTableView:(UITableView *)tableView
    {
        // determine the minimum and maximum visible rows
    
        NSArray *indexPathsForVisibleRows = [tableView indexPathsForVisibleRows];
        NSInteger minimumVisibleRow = [indexPathsForVisibleRows[0] row];
        NSInteger maximumVisibleRow = [indexPathsForVisibleRows[0] row];
    
        for (NSIndexPath *indexPath in indexPathsForVisibleRows)
        {
            if (indexPath.row < minimumVisibleRow) minimumVisibleRow = indexPath.row;
            if (indexPath.row > maximumVisibleRow) maximumVisibleRow = indexPath.row;
        }
    
        // now iterate through our model;
        // `self.objects` is an array of `TableModelRow` objects, one object
        // for every row of the table.
    
        [self.objects enumerateObjectsUsingBlock:^(TableModelRow *obj, NSUInteger idx, BOOL *stop) {
            NSAssert([obj isKindOfClass:[TableModelRow class]], @"Expected TableModelRow object");
    
            // if the index is within `kPrefetchRowCount` rows of our visible rows, let's
            // fetch the image, if it hasn't already done so.
    
            if ((idx < minimumVisibleRow && idx >= (minimumVisibleRow - kPrefetchRowCount)) ||
                (idx > maximumVisibleRow && idx <= (maximumVisibleRow + kPrefetchRowCount)))
            {
                // my model object has method for initiating a download if needed
    
                [obj downloadImageIfNeeded];
            }
        }];
    }
    
  3. In the downloading routine, you can check to see if the image download has started and, if not, then start it. To do this with SDWebImage, I keep a weak pointer to the web image operation in my TableModelRow class (the model class that backs the individual rows of my table):

    @property (nonatomic, weak) id<SDWebImageOperation> webImageOperation;
    

    I then have the downloadImageIfNeeded routine start a download if it hasn't already (you can see why making that weak was so important ... I'm checking to see if this row already has an operation pending before starting another). I'm not doing anything with the downloaded image (short of, for debugging purposes, logging the fact that a download was done), but rather just downloading and letting SDImageWeb keep track of the cached image for me, so when cellForRowAtIndexPath later requests the image as the user scrolls down, it's there, ready and waiting.

    - (void)downloadImageIfNeeded
    {
        if (self.webImageOperation)
            return;
    
        SDWebImageManager *imageManager = [SDWebImageManager sharedManager];
    
        self.webImageOperation = [imageManager downloadWithURL:self.url
                                                       options:0
                                                      progress:nil
                                                     completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
                                                         NSLog(@"%s: downloaded %@", __FUNCTION__, self.title);
                                                         // I'm not going to do anything with the image, but `SDWebImage` has now cached it for me
                                                     }];
    }
    

    Part of me thinks it might be more robust to call imageManager.imageCache instance method queryDiskCacheForKey first, but after doing some testing, it doesn't look like that's needed (and the downloadWithURL does that for us, anyway).

I should point out that the SDImageWeb library does have a SDWebImagePrefetcher class (see the documentation). The name of the class is incredibly promising, but looking at the code, with all deference to an otherwise excellent library, this doesn't feel very robust to me (e.g. it is a simple list of URLs to fetch and if you do it again, it cancels the prior list with no notion of "adding to the queue" or anything like that). It's a promising notion, but a little weak in execution. And when I tried it, my UX suffered noticeably.

So, I'm inclined to not use SDWebImagePrefetcher (until it's improved, at least), and stick to my rudimentary prefetching technique. It's not terribly sophisticated, but it seems to work.

like image 86
Rob Avatar answered Nov 13 '22 12:11

Rob


I just had to solve this exact problem and didn't want the overhead of the prefetcher. There must be some extra under-the-hood stuff happening with the built-in imageView property that prevents the loading, because a new UIImageView works just fine.

My solution is pretty clean if you don't mind (or are already) using a subclass of UITableViewCell:

  1. Subclass UITableViewCell.
  2. In your subclass, hide self.imageView.
  3. Create your own UIImageView subview and set this view's image.

Here's a modified version of my own code (undocumented here is setting the frame to match the size & position of the iOS Photo app's album covers):

YourTableCell.h

@interface YourTableCell : UITableViewCell
    @property (nonatomic, strong) UIImageView *coverPhoto;
@end

YourTableCell.m

@implementation YourTableCell

@synthesize coverPhoto;

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.imageView.image = nil;
        self.coverPhoto = [[UIImageView alloc] init];

        // Any customization, such as initial image, frame bounds, etc. goes here.        

        [self.contentView addSubview:self.coverPhoto];
    }
    return self;
}
//...
@end

YourTableViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    YourTableCell *cell = (YourTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    //...
    [cell.coverPhoto setImageWithURL:coverUrl placeholderImage:nil options:SDWebImageCacheMemoryOnly];
    //...
}
like image 31
Zak Arntson Avatar answered Nov 13 '22 12:11

Zak Arntson