SDWebImage не загружает удаленные изображения до прокрутки

Я использую библиотеку SDWebImage для загрузки удаленных изображений в табличное представление, которое использует собственный класс я создал. Я просто использую

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

в cellForRowAtIndexPath: Теперь проблема заключается в том, что он загружает изображения только в видимые ячейки, а не для ячеек, которые находятся вне экрана, для которых мне приходится прокручивать вверх и вниз, чтобы они загружались. Есть ли способ загрузить все изображения без прокрутки табличного представления. Спасибо заранее!

Ответ 1

Если вы хотите предварительно выбрать строки, вы можете ответить на методы UIScrollViewDelegate, чтобы определить, когда прокрутка таблицы завершена, вызывая предварительную выборку строк. Вы можете выполнить предварительную выборку с помощью SDWebImagePrefetcher (в моем первоначальном ответе я немного отклонил этот полезный класс, но, похоже, он работает относительно хорошо):

- (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

Вот простой cellForRowAtIndexPath (не совсем соответствующий, но просто показывающий, что если вы используете SDWebImagePrefetcher, вам не нужно возиться с 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;
}

Эти методы UIScrollViewDelegate предваряют выборку строк при прокрутке

- (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];
}

Вам, очевидно, необходимо реализовать процедуру предварительной выборки. Это получает значения NSIndexPath для ячеек с каждой стороны видимых ячеек, получает их URL-адреса изображений, а затем предварительно выбирает эти данные.

/** 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];
    }
}

Это утилиты для получения NSIndexPath для строк, непосредственно предшествующих видимым ячейкам, а также тех, которые непосредственно следуют за видимыми ячейками:

/** 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;
}

Там много, но на самом деле SDWebImage и его SDWebImagePrefetcher делает тяжелый подъем.

Я включил свой первоначальный ответ ниже для полноты.


Оригинальный ответ:

Если вы хотите сделать предварительную выборку с помощью SDWebImage, вы можете сделать что-то вроде следующего:

  • Добавьте блок завершения к вызову setImageWithURL:

    - (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;
    }
    

    Должен признаться, мне не очень нравится называть мою процедуру prefetcher здесь (я хочу, чтобы у iOS был хороший метод делегата didFinishTableRefresh), но он работает, даже если он вызывает процедуру больше раз, чем я действительно хотеть. Я просто убедился ниже, что нижеприведенная процедура гарантирует, что он не будет делать избыточные запросы.

  • В любом случае, я пишу процедуру предварительной выборки, которая ищет, скажем, следующие десять изображений:

    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];
            }
        }];
    }
    
  • В процедуре загрузки вы можете проверить, запущена ли загрузка изображений, а если нет, запустите ее. Чтобы сделать это с помощью SDWebImage, я сохраняю указатель weak для операции веб-изображения в моем классе TableModelRow (класс модели, который поддерживает отдельные строки моей таблицы):

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

    Затем я запускаю загрузку downloadImageIfNeeded, если она еще не была (вы можете понять, почему создание этого weak было настолько важным... Я проверяю, есть ли эта строка уже в ожидании операции перед началом другого). Я ничего не делаю с загруженным изображением (за исключением, для целей отладки, регистрируя факт загрузки), а просто просто загружая и позволяя SDImageWeb отслеживать кэшированное изображение для меня, поэтому, когда cellForRowAtIndexPath позже запрашивает изображение, когда пользователь прокручивает вниз, он там, готов и ждет.

    - (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
                                                     }];
    }
    

    Часть меня думает, что сначала можно было бы вызвать метод экземпляра imageManager.imageCache queryDiskCacheForKey, но после некоторого тестирования это не похоже на то, что нужно (и downloadWithURL делает это для нас, в любом случае).

Я должен указать, что библиотека SDImageWeb имеет класс SDWebImagePrefetcher (см. документацию). Название класса невероятно перспективно, но, глядя на код, со всем уважением к отличной библиотеке, это не очень удобно для меня (например, это простой список URL-адресов для извлечения, и если вы сделаете это снова, он отменяет предыдущий список без понятия "добавление в очередь" или что-то в этом роде). Это многообещающее понятие, но немного слабое в исполнении. И когда я попробовал, мой UX заметно пострадал.

Итак, я склонен не использовать SDWebImagePrefetcher (пока он не улучшится, по крайней мере), и придерживаться моей рудиментарной техники предварительной выборки. Это не очень сложно, но, похоже, это работает.

Ответ 2

Мне просто пришлось решить эту точную проблему и не хотел накладных расходов на prefetcher. Должно быть какое-то дополнительное свойство под капотом, происходящее со встроенным свойством imageView, которое предотвращает загрузку, потому что новый UIImageView работает очень хорошо.

Мое решение довольно чисто, если вы не возражаете (или уже) с помощью подкласса UITableViewCell:

  • Подкласс UITableViewCell.
  • В вашем подклассе скройте self.imageView.
  • Создайте собственное представление UIImageView и установите это изображение.

Здесь измененная версия моего собственного кода (недокументированная здесь означает, что кадр соответствует размеру и положению обложек альбомов iOS Photo):

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];
    //...
}

Ответ 3

Это пример, и вам нужно реализовать это для своей цели.
ваш делегат UITableView:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    YourCustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"YourCustomTableViewCellReuseIdentifier"];

    if (!cell)
    {
        cell = [[[YourCustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                               reuseIdentifier:CellIdentifier];        
    }

    NSString *imageURL = // ... get image url, typically from array
    [cell loadImageWithURLString:imageURL forIndexPath:indexPath]; 

    return cell;
}

ваш пользовательский файл UITableViewCell.h.:

#import <UIKit/UIKit.h>
#import "UIImageView+WebCache.h"
#import "SDImageCache.h"

@interface YourCustomTableViewCell
{
    NSIndexPath *currentLoadingIndexPath;
}

- (void)loadImageWithURLString:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath;

@end

ваш пользовательский файл UITableViewCell.m:

// ... some other methods

- (void)loadImageWithURLString:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath
{
    currentLoadingIndexPath = indexPath;
    [self.imageView cancelCurrentImageLoad];
    [self.imageView setImage:nil];

    NSURL *imageURL = [NSURL URLWithString:urlString];
    [self.imageView setImageWithURL:imageURL
                   placeholderImage:nil
                            options:SDWebImageRetryFailed
                          completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType)
    {
        if (currentLoadingIndexPath != indexPath)
        {
            return;
        }

        if (error)
        {
            ... // handle error
        }
        else
        {
            [imageView setImage:image];
        }
    }];
}

// ... some other methods

currentLoadingIndexPath необходимо определить, будем ли мы повторно использовать эту ячейку для другого изображения вместо изображения, которое было загружено, когда пользователь прокручивает представление таблицы.

Ответ 4

Я встретил ту же проблему, я обнаружил, что UIImageView + WebCache отменил последнюю загрузку при поступлении новой загрузки.

Я не уверен, является ли это намерением автора. Поэтому я пишу новую category базу UIImageView на SDWebImage.

Прост в использовании:

[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) {

                         }];

Чтобы узнать больше: ImageDownloadGroup

Расширенное использование:

//  create customGroup
MQImageDownloadGroup *customGroup = [[MQImageDownloadGroup alloc] initWithGroupIdentifier:@"tableViewCellGroup"];
customGroup.maxConcurrentDownloads = 99;

//  add to MQImageDownloadGroupManage
[[MQImageDownloadGroupManage shareInstance] addGroup:customGroup];

//  use download group
[cell.imageView mq_setImageWithURL:@"https://xxx"
                   groupIdentifier:@"tableViewCellGroup"
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {

                         }];