1

I want to build an image downloader for my UITableView so images can be loaded asynchronously into the table view.

With NSURLSession automatically using NSURLCache to cache HTTP requests, will I be able to depend on that solely as my caching mechanism? I notice many use NSCache to save the downloaded image, but if NSURLSession already implements a cache, is that necessary? If anything, isn't it bad, as I'm duplicating a copy of the image in memory?

Furthermore, if it's possible, how do I use this in conjunction with UITableView? Cell reuse presents an interesting issue with assigning the result of a background transfer.

I can't simply download the image using NSURLSessionDownloadTask and in the completion block set the cell's image, as by the time it downloads the cell may have been reused and thus causing it to set it to the wrong cell.

let downloadTask = session.downloadTaskWithURL(URL, completionHandler: { location, response, error in
    var dataFetchingError: NSError? = nil
    let downloadedImage = UIImage(data: NSData.dataWithContentsOfURL(location, options: nil, error: &dataFetchingError))

    dispatch_async(dispatch_get_main_queue()) {
        // Might end up on the wrong cell
        cell.thumbnail.image = downloadedImage
    }
})

So how do I make sure that the assigned image makes its way to the correct cell, and that I'm caching these downloads properly?

I'm aware of SDWebImage and the like, but I'd like to try to solve this problem without the use of a library.

Doug Smith
  • 29,668
  • 57
  • 204
  • 388
  • https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/bk2ch24p842downloader/ch37p1099downloader – matt Oct 13 '14 at 04:27

1 Answers1

3

You can just have the final dispatch to the main queue see if the cell was still visible before updating the image cell. You can do this using cellForRowAtIndexPath (not to be confused with the similarly named UITableViewDataSource method):

dispatch_async(dispatch_get_main_queue()) {
    if let updateCell = tableView.cellForRowAtIndexPath(indexPath) as? MyCell {
        updateCell.thumbnail.image = downloadedImage
    }
}

(Note, this assumes that the index path of the cell cannot possibly change while the asynchronous update is in progress. If it's possible that you can insert/delete rows while the update is in progress, you should not just use the old indexPath, but rather go back to the model and recalculate the appropriate index path for this cell.)

Obviously, those UIImageView categories offer other advantages besides just updating the image view asynchronously. For example, if the cell is reused, these categories will cancel any old pending network requests for that cell. This is important so that if you scroll quickly, the currently visible cells will otherwise get backlogged behind download requests for cells that may have long since scrolled out of view.

But if you're only concerned about the making sure that a cell is still visible before updating the image, the above should address that.


Regarding your cache question, there are two different types of cache to discuss: RAM cache and persistent storage cache. Both SDWebImage and AFNetworking UIImageView categories implemented their own RAM cache (using NSCache) for performance. For persistent storage caching, though, SDWebImage implemented their own, and AFNetworking argued that one should rely on the built-in NSURLCache for that. I'm sympathetic to the SDWebImage perspective because (a) historically NSURLConnection caching to persistent storage on iOS was inconsistent; (b) caching can easily be disturbed by the server using the wrong headers in the responses; and (c) Apple has been annoying opaque on the criteria for when something gets cached and when it doesn't.

So bottom line, there's probably a good argument to implement a NSCache mechanism if you want silky-smooth scrolling, but you might be able to get away with relying on NSURLCache for persistent storage caching.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • How is that method different from the data source one? – Doug Smith Oct 13 '14 at 17:28
  • The `func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell` is the one your implement in your `UITableViewDataSource`, which is what instantiates the `UITableViewCell` for a new cell. The `UITableView` method, `func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?`, is the one you call to return an "object representing a cell of the table or `nil` if the cell is not visible or `indexPath` is out of range." – Rob Oct 13 '14 at 17:45
  • Gotcha. Does `UITableView` then only dequeue cells that are going to be visible next? (It wouldn't ask for a few ahead of time?) Otherwise, if this method only tells us if they're visible, and they're not visible, but that index path has been dequeued, we'd have given it no chance to set it, right? – Doug Smith Oct 14 '14 at 02:03
  • No, try it out and you'll see that it works fine. BTW, I really like the `UIImageView` category implementation of AFNetworking and SDWebImage, so even if you don't use those classes, take a look at their basic design. It bears a certain elegance (and completely avoids this theoretical concern you have). – Rob Oct 14 '14 at 03:54
  • Is [this AFNetworking's](https://github.com/AFNetworking/AFNetworking/blob/master/UIKit%2BAFNetworking/UIImageView%2BAFNetworking.m)? Where does it do this? – Doug Smith Oct 15 '14 at 03:52
  • Yep, that's it. If you include that header at the top of your `.m` file, then you can do `setImageWithURL` and it will asynchronously retrieve the image, cancel previous one, do caching, etc. – Rob Oct 15 '14 at 03:58
  • How does it handle the logic for table views though? Where it would set it for the right cell always and handle if the cell was scrolled off screen? It looks like a pretty regular image downloader from what I can tell, and the same for SDWebImage. – Doug Smith Oct 15 '14 at 04:08
  • What "table view logic" are you looking for? A properly implemented `UIImageView` category eliminates tons of it. – Rob Oct 15 '14 at 04:31
  • I don't understand how a UIImageView category alleviates my concerns. How does it deal with the issues that arise with table views, such as when the cell scrolls off screen it should stop that cell's image download. How does a UIImageView category deal with that? – Doug Smith Oct 15 '14 at 19:36
  • Because when the cell rolls off the screen, it's dequeued and available for the next call to `cellForRowAtIndexPath`, and when the next cell calls `setImageWithURL`, it cancels the prior request. In AFNetworking, the first thing `setImageWithURL` does is `cancelImageRequestOperation` of the previous operation. (SDWebImage is the same, but it cancels via `sd_cancelCurrentImageLoad`.) It's elegant in its simplicity. – Rob Oct 15 '14 at 19:43
  • So it only maintains one download at a time? With NSURLSession and its internal NSOperationQueue it can handle several at the same time, why would you want to limit it? Wouldn't it be better to watch for `didEndDisplayingCell` and stop the corresponding download? – Doug Smith Oct 15 '14 at 20:03
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/63130/discussion-between-rob-and-doug-smith). – Rob Oct 15 '14 at 20:15