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

Seg*_*gev 151 cocoa-touch objective-c uitableview ios

我写了两种方法来在我的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;
}
Run Code Online (Sandbox Code Playgroud)

......

- (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;
}
Run Code Online (Sandbox Code Playgroud)

Rob*_*Rob 223

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

- (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;
}
Run Code Online (Sandbox Code Playgroud)

上面的代码解决了一个问题,这个问题源于该单元被重用的事实:

  1. 在启动后台请求之前,您没有初始化单元格图像(这意味着在下载新图像时,出列单元格的最后一个图像仍然可见).确保所有图像视图nilimage属性,否则您将看到图像的闪烁.

  2. 一个更微妙的问题是,在一个非常慢的网络上,您的异步请求可能无法在单元格滚出屏幕之前完成.您可以使用该UITableView方法cellForRowAtIndexPath:(不要与类似命名的UITableViewDataSource方法混淆tableView:cellForRowAtIndexPath:)来查看该行的单元格是否仍然可见.nil如果单元格不可见,则此方法将返回.

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

  3. 与手头的问题有些无关,我仍然觉得有必要更新它以利用现代约定和API,特别是:

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

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

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

即使进行了这些更正,也存在以下问题:

  1. 上面的代码没有缓存下载的图像.这意味着如果您在屏幕上滚动图像并返回屏幕,应用程序可能会尝试再次检索图像.也许你会很幸运,你的服务器响应头将允许NSURLSession和提供相当透明的缓存NSURLCache,但如果没有,你将会做出不必要的服务器请求并提供一个慢得多的用户体验.

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

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

  • 尝试了以上所有和SDWebImage(实际上已停在此处,甚至不需要尝试AFNetworking),这是迄今为止最好的选择.谢谢@Rob. (2认同)

Nit*_*rad 15

/*我这样做了,还测试了它*/

步骤1 =在viewDidLoad方法中为这样的表注册自定义单元类(如果是表格中的原型单元格)或nib(对于自定义单元格的自定义nib)

[self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"];
Run Code Online (Sandbox Code Playgroud)

要么

[self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"];
Run Code Online (Sandbox Code Playgroud)

步骤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;
        }
Run Code Online (Sandbox Code Playgroud)


kea*_*ean 13

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

迅速:

Objective-C的:

  • 实际上`SDWebImage`并没有解决这个问题.您可以控制何时下载图像,但是`SDWebImage`将图像分配给`UIImageView`而不询问您是否允许执行此操作.基本上,这个库仍然没有解决问题的问题. (2认同)

小智 7

迅捷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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用范例

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
}
Run Code Online (Sandbox Code Playgroud)

  • 我会说谢谢你。但是代码有点问题。如果让数据=尝试?Data(contentsOf:url){//请替换url到位置。这会帮助很多人。 (3认同)
  • 使用原样的代码,您可以通过网络下载两次文件:一次在 downloadTaks 中,一次使用 Data(cntentsOf:)。您必须使用用户位置来代替 url,因为下载任务实际上只是通过网络下载并将数据写入临时文件,然后将 localUrl(在您的情况下为位置)传递给您。所以 Data 需要指向本地 Url 以便它只从文件中读取。 (2认同)

Cha*_*lva 5

这是 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()
            }
Run Code Online (Sandbox Code Playgroud)