Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Download and cache images in UITableViewCell

Note: Please no libraries. This is important for me to learn. Also, there are a variety of answers on this but none that I found solves the issue nicely. Please don't mark as duplicate. Thanks in advance!

The problem I have is that if you scroll really fast in the table, you will see old images and flickering.

  • The solution from the questions I read is to cancel the URLSession data request. But I do not know how to do that at the correct place and time. There might be other solutions but not sure.

This is what I have so far:

Image cache class

class Cache {

    static let shared = Cache()

    private let cache = NSCache<NSString, UIImage>()
    var task = URLSessionDataTask()
    var session = URLSession.shared

    func imageFor(url: URL, completionHandler: @escaping (image: Image? error: Error?) -> Void) {
            if let imageInCache = self.cache.object(forKey: url.absoluteString as NSString)  {
                completionHandler(image: imageInCache, error: nil)
                return
            }

            self.task = self.session.dataTask(with: url) { data, response, error in

                if let error = error {
                    completionHandler(image: nil, error: Error)
                    return
                }

                let image = UIImage(data: data!)

                    self.cache.setObject(image, forKey: url.absoluteString as NSString)
                    completionHandler(image: image, error: nil)
                }

            self.task.resume()
    }
}

Usage

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    let myImage = images[indexPath.row]

    if let imageURL = URL(string: myImage.urlString) {
        photoImageView.setImage(from: imageURL)
    }
    return cell
}

Any thoughts?

like image 391
JEL Avatar asked Jun 25 '17 05:06

JEL


People also ask

Can images be cached?

Image caching essentially means downloading an image to the local storage in the app's cache directory (or any other directory that is accessible to the app) and loading it from local storage next time the image loads.

Should images be cached?

Generally yes, images should be cached in at least memory, and depending on your app (how likely is it to be reused,etc) in memory and storage. If you want to support your 3rd point (displaying when offline), you need to do storage caching, and memory caching is optional but probably a good idea.

What is image caching in Swift?

Cache is a hardware or software component that stores data so future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation, or the duplicate of data stored elsewhere.


2 Answers

Swift 3:

Flickering can be avoided by this way:

Use the following code in public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

cell.photoImageView.image = nil //or keep any placeholder here
cell.tag = indexPath.row
let task = URLSession.shared.dataTask(with: imageURL!) { data, response, error in
    guard let data = data, error == nil else { return }

    DispatchQueue.main.async() {
        if cell.tag == indexPath.row{
            cell.photoImageView.image = UIImage(data: data) 
        }
    }
}
task.resume()

By checking cell.tag == indexPath.row, we are assuring that the imageview whose image we are changing, is the same row for which the image is meant to be. Hope it helps!

like image 175
Mamta Avatar answered Oct 23 '22 04:10

Mamta


A couple of issues:

  1. One possible source of flickering is that while you're updating the image asynchronously, you really want to clear the image view first, so you don't see images for prior row of reused/dequeued table view cell. Make sure to set the image view's image to nil before initiating the asynchronous image retrieval. Or, perhaps combine that with "placeholder" logic that you'll see in lots of UIImageView sync image retrieval categories.

    For example:

    extension UIImageView {
    
        func setImage(from url: URL, placeholder: UIImage? = nil) {
            image = placeholder               // use placeholder (or if `nil`, remove any old image, before initiating asynchronous retrieval
    
            ImageCache.shared.image(for: url) { [weak self] result in
                switch result {
                case .success(let image):
                    self?.image = image
    
                case .failure:
                    break
                }
            }
        }
    }
    
  2. The other issue is that if you scroll very quickly, the reused image view may have an old image retrieval request still in progress. You really should, when you call your UIImageView category's async retrieval method, you should cancel and prior request associated with that cell.

    The trick here is that if you're doing this in a UIImageView extension, you can't just create new stored property to keep track of the old request. So you'd often use "associated values" to keep track of prior requests.

like image 6
Rob Avatar answered Oct 23 '22 05:10

Rob