The scroll performance of my app when it downloads high resolutions images from IMGUR is slow and sluggish. Images are oddly dequeuing and rendering. Below is the code that downloads the image. I don’t think the caching mechanism is working. How do I fix this?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PhotoTableViewCell", for: indexPath) as! PhotoTableViewCell
cell.descrpiptionLabel.text = photos[indexPath.row].title ?? "No Description"
cell.photoImageView.downloadImage(from: images[indexPath.row])
return cell
}
let imageCache = NSCache<NSString,AnyObject>()
extension UIImageView {
func downloadImage(from urlString: String ) {
guard let url = URL(string: urlString) else { return }
storeCache(url: url)
}
func storeCache(url:URL){
if let cachedImage = imageCache.object(forKey: url.absoluteString as NSString) as? UIImage {
self.image = cachedImage
}else {
let _: Void = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let self = self else { return }
if error != nil { return }
DispatchQueue.main.async {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: url.absoluteString as NSString)
self.image = downloadedImage
}
}
}.resume()
}
}
}
2
Answers
func prepareForReuse()
inPhotoTableViewCell
and setphotoImageView.image = nil
.There is a performance issue here, namely if you scroll quickly, the images for latter cells will get backlogged behind image requests for cells that are no longer visible.
To fix this, the
UIImageView
extension should save a reference to theURLSessionDataTask
, anddownloadImage
should see if there is an request in progress, and if it is for a different URL (presumably for a cell that is no longer visible), it should cancel that request before starting another network request for the visible cell.The only trick is that extensions cannot add stored properties, so you will have to use “associated objects” (e.g.,
objc_getAssociatedObject
andobjc_setAssociatedObject
) to keep track of these references.See downloading and caching images from url asynchronously for example implementation.
Unrelated to your question,
downloadImage
should, before initiating an asynchronous network request, set theimage
tonil
or to a placeholder image. If a cell is reused, the last image will be shown until the download is done. Make sure to initialize theimage
before starting an asynchronous process, to make sure you don’t see the old image until the new one is retrieved.Above I answered the performance question if you scroll quickly, where images will appear very slowly. That having been said, there is a second performance issue, namely that you are caching and using images that are larger than what is required by your UI. This can cause a different performance problem, notably hitches in your UI as you scroll (momentary hesitations that will prevent scrolling from happening smoothly).
If you see this (and presuming you can’t just request thumbnails from your backend, which is the best solution), you should resize your images to thumbnail size when you cache them. Or, if you will need the high resolution images later, have two caches, one for the full size images and another for the thumbnails.
Let’s say your image view is 50×50pt on a retina device with 3× scaling. So you will want to resize your downloaded image to 150×150px, and cache that.
If you use much larger assets for small image views, the device will have to continue to resize them on the fly, which, if the images are very large, is so computationally expensive that you will see the hitches in the scrolling behavior. If your images are just a little too big, resizing on the fly will probably not be a problem. But if you don’t have smooth scrolling and the assets are very big, resize them before caching.