从UITableView单元格中的URL加载asynchronous图像 – 滚动时图像更改为错误的图像

我已经写了两种方法来asynchronous加载我的UITableView单元格中的图片。 在这两种情况下,图像加载正常,但是当我滚动表格时,图像将会改变几次,直到滚动结束,图像将返回到正确的图像。 我不知道为什么会这样。

#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) - (void)viewDidLoad { [super viewDidLoad]; dispatch_async(kBgQueue, ^{ NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString: @"http://myurl.com/getMovies.php"]]; [self performSelectorOnMainThread:@selector(fetchedData:) withObject:data waitUntilDone:YES]; }); } -(void)fetchedData:(NSData *)data { NSError* error; myJson = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; [_myTableView reloadData]; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { // Return the number of sections. return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ // Return the number of rows in the section. // Usually the number of items in your array (the one that holds your list) NSLog(@"myJson count: %d",[myJson count]); return [myJson count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (cell == nil) { cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } dispatch_async(kBgQueue, ^{ NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"Images/Image.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]]; dispatch_async(dispatch_get_main_queue(), ^{ cell.poster.image = [UIImage imageWithData:imgData]; }); }); return cell; } 

……

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"]; if (cell == nil) { cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"]; } NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"Images/Image.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]; NSURLRequest* request = [NSURLRequest requestWithURL:url]; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse * response, NSData * data, NSError * error) { if (!error){ cell.poster.image = [UIImage imageWithData:data]; // do whatever you want with image } }]; return cell; } 

假设你正在寻找一个快速的战术修复,你需要做的是确保细胞图像被初始化,并且单元格的行仍然可见,例如:

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"]; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"Images/Image.jpg", self.myJson[indexPath.row][@"movieId"]]]; NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (data) { UIImage *image = [UIImage imageWithData:data]; if (image) { dispatch_async(dispatch_get_main_queue(), ^{ MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath]; if (updateCell) updateCell.poster.image = image; }); } } }]; [task resume]; return cell; } 

上面的代码解决了单元被重用的一些问题:

  1. 在启动后台请求之前(即在下载新图像时,出队单元的最后一个图像仍然可见),则不会初始化单元图像。 确保nil任何图像视图的image属性,否则你会看到图像的闪烁。

  2. 一个更微妙的问题是,在一个非常缓慢的networking上,您的asynchronous请求可能不会在单元格滚动屏幕之前完成。 您可以使用UITableView方法cellForRowAtIndexPath:不要与名称相同的UITableViewDataSource方法tableView:cellForRowAtIndexPath:混淆),以查看该行的单元格是否仍然可见。 如果单元格不可见,则此方法将返回nil

    问题在于,在asynchronous方法完成的时候,单元格已经滚动,更糟糕的是,单元格已被重新用于表的另一行。 通过检查行是否仍然可见,您将确保您不会意外更新自从滚动屏幕之后的行的图像。

  3. 与手头的问题有些不相干的是,我仍然不得不更新这个来利用现代公约和API,特别是:

    • 使用NSURLSession而不是调度-[NSData contentsOfURL:]到后台队列;

    • 使用dequeueReusableCellWithIdentifier:forIndexPath:而不是dequeueReusableCellWithIdentifier:但是确保使用单元格原型或注册类或NIB作为该标识符); 和

    • 我使用了符合Cocoa命名约定的类名(即以大写字母开头)。

即使有这些更正,也有问题:

  1. 上面的代码没有caching下载的图像。 这意味着,如果您将图像从屏幕上滚动回屏幕,应用程序可能会尝试再次获取图像。 也许你会很幸运,你的服务器响应头文件将允许NSURLSessionNSURLCache提供的相当透明的caching,但是如果没有的话,你将会提出不必要的服务器请求,并提供更慢的用户体验。

  2. 我们不取消对滚动屏幕的单元格的请求。 因此,如果您快速滚动到第100行,那么该行的映像可能会滞留在对以前的99行不再可见的请求之后。 您总是希望确保优先考虑可见单元的请求,以获得最佳用户体验。

解决这些问题的最简单的解决方法是使用UIImageView类别,例如SDWebImage或AFNetworking提供的类别。 如果你愿意,你可以编写你自己的代码来处理上述问题,但这是很多工作,上面的UIImageView类已经为你做了这个。

/ *我这样做了,也testing了它* /

步骤1 =在viewDidLoad方法中为这样的表注册自定义单元类(对于表中的原型单元格)或者笔尖(对于自定义单元格自定义笔尖的情况):

 [self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"]; 

要么

 [self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"]; 

步骤2 =使用UITableView的“dequeueReusableCellWithIdentifier:forIndexPath:”方法(为此,您必须注册class或nib):

  - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { CustomTableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell" forIndexPath:indexPath]; cell.imageViewCustom.image = nil; // [UIImage imageNamed:@"default.png"]; cell.textLabelCustom.text = @"Hello"; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // retrive image on global queue UIImage * img = [UIImage imageWithData:[NSData dataWithContentsOfURL: [NSURL URLWithString:kImgLink]]]; dispatch_async(dispatch_get_main_queue(), ^{ CustomTableViewCell * cell = (CustomTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]; // assign cell image on main thread cell.imageViewCustom.image = img; }); }); return cell; } 

有多个框架可以解决这个问题。 仅举几例:

迅速:

  • Nuke (我的)
  • 翠鸟
  • AlamofireImage
  • HanekeSwift

Objective-C的:

  • AFNetworking
  • PINRemoteImage
  • YYWebImage
  • SDWebImage

Swift 3

我使用NSCache编写自己的图像加载器的轻量级实现。 没有细胞图像闪烁!

ImageCacheLoader.swift

 typealias ImageCacheLoaderCompletionHandler = ((UIImage) -> ()) class ImageCacheLoader { var task: URLSessionDownloadTask! var session: URLSession! var cache: NSCache<NSString, UIImage>! init() { session = URLSession.shared task = URLSessionDownloadTask() self.cache = NSCache() } func obtainImageWithPath(imagePath: String, completionHandler: @escaping ImageCacheLoaderCompletionHandler) { if let image = self.cache.object(forKey: imagePath as NSString) { DispatchQueue.main.async { completionHandler(image) } } else { /* You need placeholder image in your assets, if you want to display a placeholder to user */ let placeholder = #imageLiteral(resourceName: "placeholder") DispatchQueue.main.async { completionHandler(placeholder) } let url: URL! = URL(string: imagePath) task = session.downloadTask(with: url, completionHandler: { (location, response, error) in if let data = try? Data(contentsOf: url) { let img: UIImage! = UIImage(data: data) self.cache.setObject(img, forKey: imagePath as NSString) DispatchQueue.main.async { completionHandler(img) } } }) task.resume() } } } 

用法示例

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier") cell.title = "Cool title" imageLoader.obtainImageWithPath(imagePath: viewModel.image) { (image) in // Before assigning the image, check whether the current cell is visible if let updateCell = tableView.cellForRow(at: indexPath) { updateCell.imageView.image = image } } return cell } 

最好的答案并不是正确的方法:(你实际上绑定了indexPath和model,这并不总是好的,想象一下在加载image的时候已经添加了一些行,现在给出indexPath的单元格存在于屏幕上,但是图像不再是正确的!情况是不太可能的,很难复制,但是这是可能的。

最好使用MVVM方法,将控制器中的viewModel与单元格绑定,并在viewModel中加载图像(使用switchToLatest方法分配ReactiveCocoa信号),然后订阅此信号并将图像分配给单元格! ;)

你必须记住不要滥用MVVM。 观点必须简单! 而ViewModels应该是可重用的! 这就是为什么在控制器中绑定View(UITableViewCell)和ViewModel非常重要。

这里是快速版本(通过使用@Nitesh Borad目标C代码): –

  if let img: UIImage = UIImage(data: previewImg[indexPath.row]) { cell.cardPreview.image = img } else { // The image isn't cached, download the img data // We should perform this in a background thread let imgURL = NSURL(string: "webLink URL") let request: NSURLRequest = NSURLRequest(URL: imgURL!) let session = NSURLSession.sharedSession() let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in let error = error let data = data if error == nil { // Convert the downloaded data in to a UIImage object let image = UIImage(data: data!) // Store the image in to our cache self.previewImg[indexPath.row] = data! // Update the cell dispatch_async(dispatch_get_main_queue(), { if let cell: YourTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? YourTableViewCell { cell.cardPreview.image = image } }) } else { cell.cardPreview.image = UIImage(named: "defaultImage") } }) task.resume() } 

在我的情况下,这不是由于图像caching(使用SDWebImage)。 这是因为自定义单元格的标签与indexPath.row不匹配。

在cellForRowAtIndexPath上:

1)为您的自定义单元格分配一个索引值。 例如,

 cell.tag = indexPath.row 

2)在主线程中,在分配图像之前,通过匹配标签来检查图像是否属于相应的单元格。

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

谢谢你“罗布”….我有同样的问题与UICollectionView和你的答案帮我解决了我的问题。 这是我的代码:

  if ([Dict valueForKey:@"ImageURL"] != [NSNull null]) { cell.coverImageView.image = nil; cell.coverImageView.imageURL=nil; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if ([Dict valueForKey:@"ImageURL"] != [NSNull null] ) { dispatch_async(dispatch_get_main_queue(), ^{ myCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath]; if (updateCell) { cell.coverImageView.image = nil; cell.coverImageView.imageURL=nil; cell.coverImageView.imageURL=[NSURL URLWithString:[Dict valueForKey:@"ImageURL"]]; } else { cell.coverImageView.image = nil; cell.coverImageView.imageURL=nil; } }); } }); } else { cell.coverImageView.image=[UIImage imageNamed:@"default_cover.png"]; } 
  - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]; cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"]; NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"Images/Image.jpg", self.myJson[indexPath.row][@"movieId"]]]; NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (data) { UIImage *image = [UIImage imageWithData:data]; if (image) { dispatch_async(dispatch_get_main_queue(), ^{ MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath]; if (updateCell) updateCell.poster.image = image; }); } } }]; [task resume]; return cell; } 

我想你想加快你的细胞加载在图像加载的细胞在背景。 为此,我们已经完成了以下步骤:

  1. 检查文件是否存在于文档目录中。

  2. 如果没有,那么第一次加载图像,并将其保存到我们的电话文档目录。 如果您不想将图像保存在手机中,则可以直接在背景中加载细胞图像。

  3. 现在加载过程:

只要包含: #import "ManabImageOperations.h"

代码就像下面的单元格一样:

 NSString *imagestr=[NSString stringWithFormat:@"http://www.yourlink.com/%@",[dictn objectForKey:@"member_image"]]; NSString *docDir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0]; NSLog(@"Doc Dir: %@",docDir); NSString *pngFilePath = [NSString stringWithFormat:@"%@/%@",docDir,[dictn objectForKey:@"member_image"]]; BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pngFilePath]; if (fileExists) { [cell1.memberimage setImage:[UIImage imageWithContentsOfFile:pngFilePath] forState:UIControlStateNormal]; } else { [ManabImageOperations processImageDataWithURLString:imagestr andBlock:^(NSData *imageData) { [cell1.memberimage setImage:[[UIImage alloc]initWithData: imageData] forState:UIControlStateNormal]; [imageData writeToFile:pngFilePath atomically:YES]; }]; } 

ManabImageOperations.h:

 #import <Foundation/Foundation.h> @interface ManabImageOperations : NSObject { } + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage; @end 

ManabImageOperations.m:

 #import "ManabImageOperations.h" #import <QuartzCore/QuartzCore.h> @implementation ManabImageOperations + (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage { NSURL *url = [NSURL URLWithString:urlString]; dispatch_queue_t callerQueue = dispatch_get_main_queue(); dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL); dispatch_async(downloadQueue, ^{ NSData * imageData = [NSData dataWithContentsOfURL:url]; dispatch_async(callerQueue, ^{ processImage(imageData); }); }); // downloadQueue=nil; dispatch_release(downloadQueue); } @end 

请检查答案和评论,如果有任何问题发生….

只需更改,

 dispatch_async(kBgQueue, ^{ NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString: [NSString stringWithFormat:@"Images/Image.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]]; dispatch_async(dispatch_get_main_queue(), ^{ cell.poster.image = [UIImage imageWithData:imgData]; }); }); 

  dispatch_async(kBgQueue, ^{ NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString: [NSString stringWithFormat:@"Images/Image.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]]; cell.poster.image = [UIImage imageWithData:imgData]; dispatch_async(dispatch_get_main_queue(), ^{ [self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; }); }); 

你可以传递你的url,

 NSURL *url = [NSURL URLWithString:@"1.png"]; NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (data) { UIImage *image = [UIImage imageWithData:data]; if (image) { dispatch_async(dispatch_get_main_queue(), ^{ yourimageview.image = image; }); } } }]; [task resume]; 
 -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ Static NSString *CellIdentifier = @"Cell"; QTStaffViewCell *cell = (QTStaffViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; If (cell == nil) { NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QTStaffViewCell" owner:self options:nil]; cell = [nib objectAtIndex: 0]; } StaffData = [self.staffArray objectAtIndex:indexPath.row]; NSString *title = StaffData.title; NSString *fName = StaffData.firstname; NSString *lName = StaffData.lastname; UIFont *FedSanDemi = [UIFont fontWithName:@"Aller" size:18]; cell.drName.text = [NSString stringWithFormat:@"%@ %@ %@", title,fName,lName]; [cell.drName setFont:FedSanDemi]; UIFont *aller = [UIFont fontWithName:@"Aller" size:14]; cell.drJob.text = StaffData.job; [cell.drJob setFont:aller]; if ([StaffData.title isEqualToString:@"Dr"]) { cell.drJob.frame = CGRectMake(83, 26, 227, 40); } else { cell.drJob.frame = CGRectMake(90, 26, 227, 40); } if ([StaffData.staffPhoto isKindOfClass:[NSString class]]) { NSURL *url = [NSURL URLWithString:StaffData.staffPhoto]; NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url completionHandler:^(NSURL *location,NSURLResponse *response, NSError *error) { NSData *imageData = [NSData dataWithContentsOfURL:location]; UIImage *image = [UIImage imageWithData:imageData]; dispatch_sync(dispatch_get_main_queue(), ^{ cell.imageView.image = image; }); }]; [task resume]; } return cell;}