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.
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?
A couple of issues:
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
}
}
}
}
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.