Search code examples
iosimageuitableviewnsurlsessionnsurlsessiondownloadtask

Downloading and showing images in the appropriate cell


I download an image per each cell I need to show in an UITableView by calling an async network task. This is in the UIViewController class of the table:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard results.count > 0 else {
        return UITableViewCell()
    }

    let myCell = tableView.dequeueReusableCell(withIdentifier: CustomCell.cellIdentifier, for: indexPath) as! CustomCell
    let model = results[indexPath.row]
    myCell.model = model
    return myCell
}

And this is CustomCell:

class CustomCell: UITableViewCell {

// Several IBOutlets

static let cellIdentifier = "myCell"
let imageProvider = ImageProvider()

var model: MyModel? {
willSet {
    activityIndicator.startAnimating()
    configureImage(showImage: false, showActivity: true)
}
didSet {
    guard let modelUrlStr = model?.imageUrlStr, let imageUrl = URL(string: modelUrlStr) else {
        activityIndicator.stopAnimating()
        configureImage(showImage: false, showActivity: false)
        return
    }

    imageProvider.getImage(imageUrl: imageUrl, completion: {[weak self] (image, error) in
        DispatchQueue.main.async {
            guard error == nil else {
                self?.activityIndicator.stopAnimating()
                self?.configureImage(showImage: false,  showActivity: false)
                return
            }

            self?.imageView.image = image
            self?.activityIndicator.stopAnimating()
            self?.configureImage(showCoverImage: true, showActivity: false)
        }
    })
}
}

override func awakeFromNib() {
super.awakeFromNib()
configureImage(showCoverImage: false, showActivity: false)
}

override func prepareForReuse() {
super.prepareForReuse()
model = nil
}

private func configureImage(showImage: Bool, showActivity: Bool) {
// Update image view
} 
}

And ImageProvider:

class ImageProvider {
var imageTask: URLSessionDownloadTask?

func getImage(imageUrl: URL, completion: @escaping DownloadResult) {
imageTask?.cancel()

imageTask = NetworkManager.sharedInstance.getImageInBackground(imageUrl: imageUrl, completion: { (image, error) -> Void in
    if let error = error {
        completion(nil, error)
    } else if let image = image {
        completion(image, nil)
    } else {
        completion(nil, nil)
    }
})
}
}

Since cells can be dynamically dequeued and reused and the download is async and then an image could be reused on each cell while scrolling, am I this way ensuring that each cell is always showing its corresponding image?

EDIT: Different approach

Is it appropriate to keep a reference to the model in the cell? Thinking about a correct MVC architecture. Who should be the responsible for downloading the images? The cell (passing to it only the URL instead of the complete model object), or the table view's view controller (updating the image in the cell in its tableView:cellForRowAt: method)?


Solution

    1. Add a method to your ImageProvider that would allow you to cancel the underlying URLSessionDownloadTask. Call this method when the cell is about to be reused. For that, override prepareForReuse() in your cell subclass.
    2. There is a possibility that the task will already finish by the time reuse happens, but the DispatchQueue.main.async block is still enqueued and will be fired after cell reuse has happened. To mitigate this, you need to check the URL that the of the finished task against the URL that is stored in your model.