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
}
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.
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()
}
})
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.
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
}
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)
}