Асинхронная загрузка изображений для UITableView с GCD

Я загружаю изображения для своего uitableview асинхронно с помощью GCD, но возникает проблема - когда прокрутка изображений мерцает и меняется все время. Я попытался установить изображение на ноль с каждой ячейкой, но это мало помогает. При быстром прокрутке все изображения ошибочны. Что я могу сделать по этому поводу? Вот мой метод для ячеек:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {

                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];

                UIImage* image = [[UIImage alloc] initWithData:imageData];

                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.imageView.image = image;
                    [cell setNeedsLayout];
                     });
            });

    cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }
    return cell;
}

Ответ 1

Проблема заключается в том, что ваши блоки захвата изображения содержат ссылки на ячейки tableview. Когда загрузка завершается, она устанавливает свойство imageView.image, даже если вы переработали ячейку для отображения другой строки.

Вам понадобится блок завершения загрузки, чтобы проверить, действительно ли изображение по-прежнему относится к ячейке перед настройкой изображения.

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

Изменить: здесь простой способ проверить, используя свойство cell tag:

- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    cell.tag = indexPath.row;
    NSDictionary *parsedData = self.loader.parsedData[indexPath.row];
    if (parsedData)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
        dispatch_async(queue, ^(void) {

            NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:parsedData[@"imageLR"]];

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

        cell.textLabel.text = parsedData[@"id"];
    }
    return cell;
}

Ответ 2

Дело в том, что вы не полностью поняли концепцию повторного использования ячеек. Это не очень хорошо согласуется с асинхронными загрузками.

Блок

    ^{
    cell.imageView.image = image;
    [cell setNeedsLayout];
}

выполняется, когда запрос завершен, и все данные были загружены. Но ячейка получает свое значение при создании блока.

К тому моменту, когда блок выполняется, ячейка все еще указывает на одну из существующих ячеек. Но вполне вероятно, что пользователь продолжил прокрутку. Объект ячейки повторно использовался в то же время, и изображение ассоциировалось со "старой" ячейкой, которая повторно используется, назначается и отображается. Вскоре после этого правильное изображение загружается, назначается и отображается, если пользователь не прокручивается дальше. И так далее, и так далее.

Вы должны искать более разумный способ сделать это. Есть много турориалов. Google для ленивой загрузки изображений.

Ответ 3

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

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {
                //  You may want to cache this explicitly instead of reloading every time.
                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];
                UIImage* image = [[UIImage alloc] initWithData:imageData];
                dispatch_async(dispatch_get_main_queue(), ^{
                    // Capture the indexPath variable, not the cell variable, and use that
                    UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
                    blockCell.imageView.image = image;
                    [blockCell setNeedsLayout];
                });
            });
        cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }

    return cell;
}

Ответ 4

Я изучаю эту проблему, и я нашел отличный подход, настроив UITableViewCell.

#import <UIKit/UIKit.h>

@interface MyCustomCell : UITableViewCell

@property (nonatomic, strong) NSURLSessionDataTask *imageDownloadTask;
@property (nonatomic, weak) IBOutlet UIImageView *myImageView;
@property (nonatomic, weak) IBOutlet UIActivityIndicatorView *activityIndicator;

@end

Теперь, в вашем TableViewController, объявите два свойства для NSURLSessionConfiguration и NSURLSession и инициализируйте их в ViewDidLoad:

@interface MyTableViewController ()

@property (nonatomic, strong) NSURLSessionConfiguration *sessionConfig;
@property (nonatomic, strong) NSURLSession *session;
.
.
.
@end

@implementation TimesVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    _session = [NSURLSession sessionWithConfiguration:_sessionConfig];
}

.
.
.

Предположим, что ваш источник данных является массивом NSMutableDictionary (или NSManagedObjectContext). Вы можете легко загрузить изображение для каждой ячейки с кешированием, например:

.
.
.
- (MyCustomCell *)tableView:(UITableView *)tableView
   cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MyCustomCell *cell = (MyCustomCell *)[tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];

    if (!cell)
    {
        cell = [[MyCustomCell alloc] initWithStyle:UITableViewCellStyleDefault
                                reuseIdentifier:@"cell"];
    }

    NSMutableDictionary *myDictionary = [_myArrayDataSource objectAtIndex:indexPath.row];    

    if (cell.imageDownloadTask)
    {
        [cell.imageDownloadTask cancel];
    }

    [cell.activityIndicator startAnimating];
    cell.myImageView.image = nil;

    if (![myDictionary valueForKey:@"image"])
    {
        NSString *urlString = [myDictionary valueForKey:@"imageURL"];
        NSURL *imageURL = [NSURL URLWithString:urlString];
        if (imageURL)
        {
            cell.imageDownloadTask = [_session dataTaskWithURL:imageURL
                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
            {
                if (error)
                {
                    NSLog(@"ERROR: %@", error);
                }
                else
                {
                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

                    if (httpResponse.statusCode == 200)
                    {
                        UIImage *image = [UIImage imageWithData:data];

                        dispatch_async(dispatch_get_main_queue(), ^{
                            [myDictionary setValue:data forKey:@"image"];
                            [cell.myImageView setImage:image];
                            [cell.activityIndicator stopAnimating];
                        });
                    }
                    else
                    {
                        NSLog(@"Couldn't load image at URL: %@", imageURL);
                        NSLog(@"HTTP %d", httpResponse.statusCode);
                    }
                }
            }];

            [cell.imageDownloadTask resume];
        }
    }
    else
    {
        [cell.myImageView setImage:[UIImage imageWithData:[myDictionary valueForKey:@"image"]]];
        [cell.activityIndicator stopAnimating];
    }

    return cell;
}

Я надеюсь, что это поможет некоторым разработчикам! Приветствия.

КРЕДИТЫ: Просмотр столов в iOS 7

Ответ 5

Не изобретайте велосипед, просто используйте эту пару удивительных стручков:

https://github.com/rs/SDWebImage

https://github.com/JJSaccolo/UIActivityIndicator-for-SDWebImage

Проще, чем:

    [self.eventImage setImageWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", [SchemeConfiguration APIEndpoint] , _event.imageUrl]]
                          placeholderImage:nil
                                 completed:^(UIImage *image,
                                             NSError *error,
                                             SDImageCacheType cacheType,
                                             NSURL *imageURL)
     {
         event.image = UIImagePNGRepresentation(image);
     } usingActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];

Ответ 6

Считаете ли вы, что используете SDWebImage? Он также загружает изображение из заданного URL-адреса асинхронно. Я не использовал всю библиотеку (импортировал только UIImageView + WebCache.h). После импорта все, что вам нужно сделать, это вызвать метод как таковой:

[UIImageXYZ sd_setImageWithURL:["url you're retrieving from"] placeholderImage:[UIImage imageNamed:@"defaultProfile.jpeg"]];

Это может быть слишком много, если вы используете AFNetworking 2.0, но это сработало для меня.

Вот ссылка на github, если вы хотите попробовать.

Ответ 7

Помимо принятого ответа Seamus Campbell, вы также должны знать, что когда-нибудь это не сработает. В этом случае мы должны перезагрузить конкретную ячейку. Так

if (image) {
     dispatch_async(dispatch_get_main_queue(), ^{
          if (cell.tag == indexPath.row) {
               cell.imageView.image = image;
               [cell setNeedsLayout];
          }
      });
 }

следует изменить,

    if (image) {
         dispatch_async(dispatch_get_main_queue(), ^{
              if (cell.tag == indexPath.row) {
                   cell.imageView.image = image;
                   self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
              }
          });
     }