ios 从 UITableView 单元格内的 url 加载异步图像 - 滚动时图像更改为错误图像
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/16663618/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Async image loading from url inside a UITableView cell - image changes to wrong image while scrolling
提问by Segev
I've written two ways to async load pictures inside my UITableView cell. In both cases the image will load fine but when I'll scroll the table the images will change a few times until the scroll will end and the image will go back to the right image. I have no idea why this is happening.
我已经编写了两种在 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:@"http://myurl.com/%@.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:@"http://myurl.com/%@.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;
}
回答by Rob
Assuming you're looking for a quick tactical fix, what you need to do is make sure the cell image is initialized and also that the cell's row is still visible, e.g:
假设您正在寻找快速的战术修复,您需要做的是确保单元格图像已初始化并且单元格的行仍然可见,例如:
- (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:@"http://myurl.com/%@.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;
}
The above code addresses a few problems stemming from the fact that the cell is reused:
上面的代码解决了由于单元格被重用而产生的一些问题:
You're not initializing the cell image before initiating the background request (meaning that the last image for the dequeued cell will still be visible while the new image is downloading). Make sure to
nil
theimage
property of any image views or else you'll see the flickering of images.A more subtle issue is that on a really slow network, your asynchronous request might not finish before the cell scrolls off the screen. You can use the
UITableView
methodcellForRowAtIndexPath:
(not to be confused with the similarly namedUITableViewDataSource
methodtableView:cellForRowAtIndexPath:
) to see if the cell for that row is still visible. This method will returnnil
if the cell is not visible.The issue is that the cell has scrolled off by the time your async method has completed, and, worse, the cell has been reused for another row of the table. By checking to see if the row is still visible, you'll ensure that you don't accidentally update the image with the image for a row that has since scrolled off the screen.
Somewhat unrelated to the question at hand, I still felt compelled to update this to leverage modern conventions and API, notably:
Use
NSURLSession
rather than dispatching-[NSData contentsOfURL:]
to a background queue;Use
dequeueReusableCellWithIdentifier:forIndexPath:
rather thandequeueReusableCellWithIdentifier:
(but make sure to use cell prototype or register class or NIB for that identifier); andI used a class name that conforms to Cocoa naming conventions(i.e. start with the uppercase letter).
在启动后台请求之前,您没有初始化单元格图像(这意味着在下载新图像时,出列单元格的最后一张图像仍然可见)。确保任何图像视图
nil
的image
属性,否则你会看到图像闪烁。一个更微妙的问题是,在非常慢的网络上,您的异步请求可能不会在单元格滚出屏幕之前完成。您可以使用该
UITableView
方法cellForRowAtIndexPath:
(不要与名称相似的UITableViewDataSource
方法混淆tableView:cellForRowAtIndexPath:
)来查看该行的单元格是否仍然可见。nil
如果单元格不可见,则此方法将返回。问题是当您的异步方法完成时单元格已经滚动,更糟糕的是,该单元格已被重新用于表格的另一行。通过检查该行是否仍然可见,您将确保不会意外地使用已滚动出屏幕的行的图像更新图像。
与手头的问题有些无关,我仍然觉得有必要更新它以利用现代约定和 API,特别是:
使用
NSURLSession
而不是调度-[NSData contentsOfURL:]
到后台队列;使用
dequeueReusableCellWithIdentifier:forIndexPath:
而不是dequeueReusableCellWithIdentifier:
(但请确保为该标识符使用单元原型或注册类或 NIB);和我使用了符合Cocoa 命名约定的类名(即以大写字母开头)。
Even with these corrections, there are issues:
即使进行了这些更正,也存在问题:
The above code is not caching the downloaded images. That means that if you scroll an image off screen and back on screen, the app may try to retrieve the image again. Perhaps you'll be lucky enough that your server response headers will permit the fairly transparent caching offered by
NSURLSession
andNSURLCache
, but if not, you'll be making unnecessary server requests and offering a much slower UX.We're not canceling requests for cells that scroll off screen. Thus, if you rapidly scroll to the 100th row, the image for that row could be backlogged behind requests for the previous 99 rows that aren't even visible anymore. You always want to make sure you prioritize requests for visible cells for the best UX.
上面的代码没有缓存下载的图像。这意味着如果您将图像滚动到屏幕外然后又回到屏幕上,应用程序可能会再次尝试检索图像。也许您会很幸运,您的服务器响应标头将允许
NSURLSession
和提供的相当透明的缓存NSURLCache
,但如果不是,您将发出不必要的服务器请求并提供更慢的用户体验。我们不会取消对滚动出屏幕的单元格的请求。因此,如果您快速滚动到第 100 行,则该行的图像可能会积压在前 99 行甚至不再可见的请求之后。您总是希望确保对可见单元格的请求进行优先级排序,以获得最佳 UX。
The simplest fix that addresses these issues is to use a UIImageView
category, such as is provided with SDWebImageor AFNetworking. If you want, you can write your own code to deal with the above issues, but it's a lot of work, and the above UIImageView
categories have already done this for you.
解决这些问题的最简单的修复方法是使用UIImageView
类别,例如随SDWebImage或AFNetworking提供的类别。如果你愿意,你可以自己写代码来处理上面的问题,但是工作量很大,上面的UIImageView
类已经为你做了这些。
回答by Nitesh Borad
/* I have done it this way, and also tested it */
/* 我是这样弄的,也测试过*/
Step 1 = Register custom cell class (in case of prototype cell in table) or nib (in case of custom nib for custom cell) for table like this in viewDidLoad method:
步骤 1 = 在 viewDidLoad 方法中为这样的表格注册自定义单元格类(如果是表格中的原型单元格)或笔尖(如果是自定义单元格的自定义笔尖):
[self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"];
OR
或者
[self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"];
Step 2 = Use UITableView's "dequeueReusableCellWithIdentifier: forIndexPath:" method like this (for this, you must register class or nib) :
步骤 2 = 像这样使用 UITableView 的“dequeueReusableCellWithIdentifier: forIndexPath:”方法(为此,您必须注册类或笔尖):
- (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;
}
回答by kean
There are multiple frameworks that solve this problem. Just to name a few:
有多种框架可以解决这个问题。仅举几个:
Swift:
迅速:
Objective-C:
目标-C:
回答by Dmitrii Klassneckii
Swift 3
斯威夫特 3
I write my own light implementation for image loader with using NSCache. No cell image flickering!
我使用 NSCache 为图像加载器编写了自己的轻量级实现。 没有细胞图像闪烁!
ImageCacheLoader.swift
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()
}
}
}
Usage example
使用示例
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
}
回答by Chathuranga Silva
Here is the swift version (by using @Nitesh Borad objective C code) :-
这是 swift 版本(通过使用 @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()
}
回答by A.G
In my case, it wasn't due to image caching (Used SDWebImage). It was because of custom cell's tag mismatch with indexPath.row.
就我而言,这不是由于图像缓存(使用 SDWebImage)。这是因为自定义单元格的标签与 indexPath.row 不匹配。
On cellForRowAtIndexPath :
在 cellForRowAtIndexPath 上:
1) Assign an index value to your custom cell. For instance,
1) 为您的自定义单元格分配一个索引值。例如,
cell.tag = indexPath.row
2) On main thread, before assigning the image, check if the image belongs the corresponding cell by matching it with the tag.
2)在主线程上,在分配图像之前,通过与标签匹配来检查图像是否属于相应的单元格。
dispatch_async(dispatch_get_main_queue(), ^{
if(cell.tag == indexPath.row) {
UIImage *tmpImage = [[UIImage alloc] initWithData:imgData];
thumbnailImageView.image = tmpImage;
}});
});
回答by badeleux
The best answer is not the correct way to do this :(. You actually bound indexPath with model, which is not always good. Imagine that some rows has been added during loading image. Now cell for given indexPath exists on screen, but the image is no longer correct! The situation is kinda unlikely and hard to replicate but it's possible.
最好的答案不是这样做的正确方法:(。您实际上将 indexPath 与模型绑定,这并不总是好的。想象一下在加载图像期间添加了一些行。现在给定 indexPath 的单元格存在于屏幕上,但图像不再正确!这种情况不太可能且难以复制,但有可能。
It's better to use MVVM approach, bind cell with viewModel in controller and load image in viewModel (assigning ReactiveCocoa signal with switchToLatest method), then subscribe this signal and assign image to cell! ;)
最好使用 MVVM 方法,在控制器中将 cell 与 viewModel 绑定并在 viewModel 中加载图像(使用 switchToLatest 方法分配 ReactiveCocoa 信号),然后订阅此信号并将图像分配给单元格!;)
You have to remember to not abuse MVVM. Views have to be dead simple! Whereas ViewModels should be reusable! It's why it's very important to bind View (UITableViewCell) and ViewModel in controller.
你必须记住不要滥用 MVVM。视图必须非常简单!而 ViewModels 应该是可重用的!这就是为什么在控制器中绑定 View (UITableViewCell) 和 ViewModel 非常重要。
回答by sneha
Thank you "Rob"....I had same problem with UICollectionView and your answer help me to solved my problem. Here is my code :
谢谢“Rob”......我在 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"];
}
回答by Dharmraj Vora
- (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:@"http://myurl.com/%@.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;
}
回答by User558
You can just pass your URL,
你可以只传递你的网址,
NSURL *url = [NSURL URLWithString:@"http://www.myurl.com/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];