tableView cell is displaying the wrong image somet

2019-03-22 17:51发布

问题:

I have a tableView that displays an image in the cell. Most of the time the correct image will be displayed, however occasionally it will display the wrong image (usually if scrolling down the tableView very quickly). I download the images asynchronously and store them in a cache. Can't find what else could be causing the issue?? Below is the code:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    // try to reuse cell
    let cell:CustomCell = tableView.dequeueReusableCellWithIdentifier("DealCell") as CustomCell

    // get the venue image
    let currentVenueImage = deals[indexPath.row].venueImageID
    let unwrappedVenueImage = currentVenueImage
    var venueImage = self.venueImageCache[unwrappedVenueImage]
    let venueImageUrl = NSURL(string: "http://notrealsite.com/restaurants/\(unwrappedVenueImage)/photo")

    // reset reused cell image to placeholder
    cell.venueImage.image = UIImage(named: "placeholder venue")

    // async image
    if venueImage == nil {

        let request: NSURLRequest = NSURLRequest(URL: venueImageUrl!)

        NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response: NSURLResponse!,data: NSData!,error: NSError!) -> Void in
            if error == nil {

                venueImage = UIImage(data: data)

                self.venueImageCache[unwrappedVenueImage] = venueImage
                dispatch_async(dispatch_get_main_queue(), {

                    // fade image in
                    cell.venueImage.alpha = 0
                    cell.venueImage.image = venueImage
                    cell.venueImage.fadeIn()

                })
            }
            else {

            }
        })
    }

    else{
        cell.venueImage.image = venueImage
    }

    return cell

}

回答1:

I think the issue is with sendAsynchronousRequest. If you are scrolling faster than this is taking, when you reuse a cell, you can end up with the old completionHandler replacing the "wrong" cell (since it's now showing a different entry). You need to check in the completion handler that it's still the image you want to show.



回答2:

So after some of the previous answers pointing me in the right direction, this is the code I added, which seems to have done the trick. The images all load and are displayed as they should now.

dispatch_async(dispatch_get_main_queue(), {

                    // check if the cell is still on screen, and only if it is, update the image.
                    let updateCell = tableView .cellForRowAtIndexPath(indexPath)
                    if updateCell != nil {

                    // fade image in
                    cell.venueImage.alpha = 0
                    cell.venueImage.image = venueImage
                    cell.venueImage.fadeIn()
                    }
                })


回答3:

This is the problem with dequeued re-usable cell. Inside the image download completion method, you should check whether this downloaded image is for correct index-path. You need to store a mapping data-structure that stores the index-path and a corresponding url. Once the download completes, you need to check whether this url belongs to current indexpath, otherwise load the cell for that downloaded-indexpath and set the image.



回答4:

Swift 3

I write my own light implementation for image loader with using NSCache. No cell image flickering!

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 for ensuring that it's right cell
        if let updateCell = tableView.cellForRow(at: indexPath) {
            updateCell.imageView.image = image
        }
    }    
    return cell
}


回答5:

The following code changes worked me.

You can download the image in advance and save it in the application directory which is not accessible by the user. You get these images from the application directory in your tableview.

// Getting images in advance
var paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! String
var dirPath = paths.stringByAppendingPathComponent("XYZ/")

var imagePath = paths.stringByAppendingPathComponent("XYZ/\(ImageName)" )
println(imagePath)
var checkImage = NSFileManager.defaultManager()

if checkImage.fileExistsAtPath(imagePath) {
  println("Image already exists in application Local")
} else {
  println("Getting Image from Remote")
  checkImage.createDirectoryAtPath(dirPath, withIntermediateDirectories: true, attributes: nil, error: nil)
  let request: NSURLRequest = NSURLRequest(URL: NSURL(string: urldata as! String)!)
  let mainQueue = NSOperationQueue.mainQueue()
  NSURLConnection.sendAsynchronousRequest(request, queue: mainQueue, completionHandler: { (response, data, error) -> Void in
    if let httpResponse = response as? NSHTTPURLResponse {
      // println("Status Code for successful------------------------------------>\(httpResponse.statusCode)")
      if (httpResponse.statusCode == 502) {
        //Image not found in the URL, I am adding default image
        self.logoimg.image = UIImage(named: "default.png")
        println("Image not found in the URL")
      } else if (httpResponse.statusCode == 404) {
        //Image not found in the URL, I am adding default image
        self.logoimg.image = UIImage(named: "default.png")
        println("Image not found in the URL")
      } else if (httpResponse.statusCode != 404) {
        // Convert the downloaded data in to a UIImage object
        let image = UIImage(data: data)
        // Store the image in to our cache
        UIImagePNGRepresentation(UIImage(data: data)).writeToFile(imagePath, atomically: true)
        dispatch_async(dispatch_get_main_queue(), {
          self.logoimg.contentMode = UIViewContentMode.ScaleAspectFit
          self.logoimg.image = UIImage(data: data)
          println("Image added successfully")
        })
      }

    }
  })
}

// Code in cellForRowAtIndexPath
var checkImage = NSFileManager.defaultManager()
if checkImage.fileExistsAtPath(imagePath) {
  println("Getting Image from application directory")
  let getImage = UIImage(contentsOfFile: imagePath)
  imageView.backgroundColor = UIColor.whiteColor()
  imageView.image = nil

  imageView.image = getImage
  imageView.frame = CGRectMake(xOffset, CGFloat(4), CGFloat(30), CGFloat(30))
  cell.contentView.addSubview(imageView)
} else {
  println("Default image")
  imageView.backgroundColor = UIColor.whiteColor()

  imageView.image = UIImage(named: "default.png")!

  imageView.frame = CGRectMake(xOffset, CGFloat(4), CGFloat(30), CGFloat(30))
  cell.contentView.addSubview(imageView)
}