Асинхронная загрузка изображений для 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)
}
});
}