3

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

}
mossy321
  • 305
  • 3
  • 12
  • Why to reset reused cell image every time? Reset is needed only if there's no `venueImage` found. – Juri Noga Aug 21 '15 at 05:34
  • 1
    http://stackoverflow.com/questions/16663618/async-image-loading-from-url-inside-a-uitableview-cell-image-changes-to-wrong – Rishi Aug 21 '15 at 05:35
  • let unwrappedVenueImage = currentVenueImage <- this does _not_ unwrap the currentVenueImage. Unwrapping is "if let ..." – Darko Aug 21 '15 at 05:38
  • use AsyncImageView instead of using UIImageView It will give autometically cache and you can use it by setting setImageWithURL pass your URL and it has spinner also in that so no need to require any placeholder image. – JAY RAPARKA Aug 21 '15 at 05:40
  • Which swift version your are using? – Shoaib Aug 21 '15 at 06:11
  • I'm using Xcode 6.1, swift 1.1 – mossy321 Aug 21 '15 at 06:27

6 Answers6

6

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
}
Community
  • 1
  • 1
5

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.

Oliver
  • 566
  • 4
  • 10
  • 1
    Thanks, I think your right. Any idea how I'd code that up? Rishi's link had the solution in obj-c, but I can't figure out how to do the same in swift. – mossy321 Aug 21 '15 at 06:26
3

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()
                    }
                })
mossy321
  • 305
  • 3
  • 12
2

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.

Tushar
  • 3,022
  • 2
  • 26
  • 26
1

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)
}
fatihyildizhan
  • 8,614
  • 7
  • 64
  • 88
Karlos
  • 1,653
  • 18
  • 36
1

Swift 5

So a simple solution to your problem would be by creating a custom class which subclasses UIImageView.

Add a property to store the url string. Initially set the image to your placeholder to stop flickering. While parsing and setting the image from response data compare class property url string with the url string passed through to the function and make sure they are equal. Cache your image with the key as the url string and retrieve accordingly.

Note: Do not extend UIImageview as we plan to add property imageUrl.

reference: https://www.youtube.com/watch?v=XFvs6eraBXM

let imageCache = NSCache<NSString, UIImage>()
class CustomIV: UIImageView {
    var imageUrl: String?
    func loadImage(urlStr: String) {
        imageUrl = urlStr
        image = UIImage(named: "placeholder venue")
        if let img = imageCache.object(forKey: NSString(string: imageUrl!)) {
            image = img    
            return
        }
        guard let url = URL(string: urlStr) else {return}
        imageUrl = urlStr
        URLSession.shared.dataTask(with: url) { data, response, error in
            if let err = error {
                print(err)
            } else {
                DispatchQueue.main.async {
                    let tempImg = UIImage(data: data!)
                    if self.imageUrl == urlStr {
                        self.image = tempImg
                    }
                    imageCache.setObject(tempImg!, forKey: NSString(string: urlStr))
                }
            }
        }.resume()
    }
}

Just update you tableview cell's imageview to a CustomIV object. And then update the image using:

cell.venueImage.loadImage(urlStr: "http://notrealsite.com/restaurants/\(unwrappedVenueImage)/photo")
Community
  • 1
  • 1
SwiftySai
  • 11
  • 3