1

I have a TableView with ImageViews inside each cell. I want the images to get loaded once and remain like that but it seems that the images get loaded (downloaded, I'm getting them from an external API) as they get into visible area for user. It seems like a lazy load or something like that and I would like to disable it because if I scroll down then come back up most of the images get misplaced.

TableViewController.swift

cell?.mainChampImageView.image = businessLayer.getChampionThumbnailImage(championId: mainChampion.key)

BusinessLayer.swift

func getChampionThumbnailImage (championId: Int) -> UIImage {
        return dataLayerRiot.getChampionThumbnailImage(championId: championId)
    }

DataLayerRiot.swift

func getChampionThumbnailImage (championId: Int) -> UIImage {
        var image: UIImage!

        let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
        let url = URL(string: urlString)
        let session = URLSession.shared

        let semaphore = DispatchSemaphore(value: 0)

        session.dataTask(with: url!) {(data, response, error) in
            if error != nil {
                print("ERROR")
                semaphore.signal()
            }
            else {
                image = UIImage(data: data!)!
                semaphore.signal()
            }
        }.resume()

        semaphore.wait()
        session.finishTasksAndInvalidate()

        return image
    }

Anyone know how to disable them loading as they get into visible area for the user and just have them "stored"?

EDIT

I am dequeuing the cell using the default way

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "Match", for: indexPath) as? TableViewCell
        ...
}

Is there a better way of doing it?

EDIT 2

I also need to specify that I am unable to install libraries because this is a university project and I am able to work only on university's MACs (because I don't own one) therefore I am unable to install packages without administrator privileges.

Cristian G
  • 460
  • 1
  • 4
  • 22
  • 2
    Are you using `dequeueReusableCell`, if so its likely that could be causing the issue. You probably need to download the images and cache them, pulling the correct one back as necessary, maybe this will help https://stackoverflow.com/a/50443730/285190 – Flexicoder Nov 09 '18 at 13:03
  • 3
    Have a look at KingFisher [https://github.com/onevcat/Kingfisher]. It's main purpose is downloading and caching images. – Alan S Nov 09 '18 at 13:10
  • @Flexicoder I added the way I am dequeuing the cell, please let me know if there is a better way of doing it. Also, Alan Sarraf, I cannot install libraries/packages because I do not have admin privileges. – Cristian G Nov 13 '18 at 11:41
  • 1
    @CristianG you probably need to implement `prepareForReuse()` https://developer.apple.com/documentation/uikit/uitableviewcell/1623223-prepareforreuse – Flexicoder Nov 13 '18 at 11:44
  • @Flexicoder I have used it and it works properly now. Thank you very much! – Cristian G Nov 13 '18 at 11:51
  • 1
    @CristianG glad it helped – Flexicoder Nov 13 '18 at 11:51

3 Answers3

2

You should save a task at memory like:

let task = = session.dataTask() {}

And after you can cancel it anywhere by:

task.cancel()

Alternatively, if the object session is a URLSession instance, you can cancel it by:

session.invalidateAndCancel()
biloshkurskyi.ss
  • 1,358
  • 3
  • 15
  • 34
  • I have looked at the differences but why do you think invalideAndCancel would be better than finishTasksAndInvalidate? – Cristian G Nov 13 '18 at 11:45
1

Try SDWebImage for lazy loading the images in the UITableViewCell or UICollectionViewCell. Install it through cocoapods into your project.

It is an asynchronous memory + disk image caching with automatic cache expiration handling.

https://github.com/SDWebImage/SDWebImage

Code:

let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
cell?.mainChampImageView.sd_setImage(with: url, placeholderImage: UIImage(named: "placeholder.png"))
Sateesh Yemireddi
  • 4,289
  • 1
  • 20
  • 37
  • I am unable to add libraries/packages because I don't have admin privileges. If you can suggest an answer without it I would greatly appreciate. :D – Cristian G Nov 13 '18 at 11:46
1

It sounds like you could benefit from doing some image caching. There are multiple ways to go about doing so, but from your example, it doesn't look like you need to go through the trouble of adding an entire library to do so. You can do it in a simple manner using NSCache.

I created a class called ImageCache, and in this case it is a singleton, so that the cache is accessible throughout the entire application.

import UIKit

class ImageCache: NSObject {
    static let sharedImageCache = ImageCache()

    // Initialize cache, specifying that your key type is AnyObject
    // and your value type is AnyObject. This is because NSCache requires 
    // class types, not value types so we can't use <URL, UIImage>

    let imageCache = NSCache<AnyObject, AnyObject>()

    // Here we store the image, with the url as the key
    func add(image: UIImage, for url: URL) {
        // we cast url as AnyObject because URL is not a class type, it's a value type
        imageCache.setObject(image, forKey: url as AnyObject)
    }


    // This allows us to access the image from cache with the URL as the key
    // (e.g. cache[URL])
    func fetchImage(for url: URL) -> UIImage? {
        var image: UIImage?

        // Casting url for the same reason as before, but we also want the result
        // as an image, so we cast that as well
        image = imageCache.object(forKey: url as AnyObject) as? UIImage
        return image
    }
}

So now we have some relatively simple caching in place. Now for how to use it:

func getChampionThumbnailImage (championId: Int) -> UIImage {
    var image: UIImage!

    let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
    let url = URL(string: urlString)

    // Before, downloading the image, we check the cache to see if it exists and is stored.
    // If so, we can grab that image from the cache and avoid downloading it again.
    if let cachedImage = ImageCache.sharedImageCache.fetchImage(for: url) {
        image = cachedImage
        return image
    }

    let session = URLSession.shared

    let semaphore = DispatchSemaphore(value: 0)

    session.dataTask(with: url!) {(data, response, error) in
        if error != nil {
            print("ERROR")
            semaphore.signal()
        }
        else {
            image = UIImage(data: data!)!
            // Once the image is successfully downloaded the first time, add it to 
            // the cache for later retrieval
            ImageCache.sharedImageCache.add(image: image, for: url!)
            semaphore.signal()
        }
    }.resume()

    semaphore.wait()
    session.finishTasksAndInvalidate()

    return image
}

The reason the images are re-downloading is because a table view doesn't have unlimited cells. What happens is, as you scroll down, the cells that go off the screen are then recycled and re-used, so when you scroll back up, the images have to be grabbed again because they've been emptied out.

You can avoid downloading the images again by implementing caching.

Another way you can avoid having incorrect images is setting your image view to nil before you re-download the image. For example:

cell?.mainChampImageView = nil
cell?.mainChampImageView.image = businessLayer.getChampionThumbnailImage(championId: mainChampion.key)

All of the above, along with making sure that you are dequeuing cells properly should address your issue.

droberson
  • 43
  • 3
  • I created the ImageCache class and used it as specified and even set the image to nil before setting it to the image I need but it still has the same behaviour. I can make the gitlab repository public if you think that taking a look at the whole project will be helpful. Also I have added the code which I use to dequeue cells, please let me know if it is the correct way. – Cristian G Nov 13 '18 at 11:38
  • I have used the prepareForReuse() as @Flexicoder suggested and it works but I will still use the cache method you showed for performance reasons. Thank you very much! – Cristian G Nov 13 '18 at 11:54
  • If you make a public reply I'd be happy to take a look at it. – droberson Nov 14 '18 at 06:29