The prefetch APIs for UITableView and UICollectionView are great for preemptively loading pages of data, as we saw in episode 315. However, to take full advantage of this API we should also leverage loading or processing data related to the cells we are about to show. One common example of this is fetching an image before the cell comes on screen. In this episode we will implement this with a handy library called Nuke. The result is quite impressive.
Episode Links UITableViewDataSourcePrefetching Documentation Nuke Adding Nuke to our project We can add Nuke by adding this to our Podfile: pod 'Nuke' And then running $ pod install Once installed, import it at the top of BeerListViewController.swift: import Nuke Using Nuke to fetch images We can use Nuke's shared Manager class to fetch images for us and have those delivered to a Target. Nuke already extends UIImageView to conform to the Target protocol, so usage is pretty simple: override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // ... let identifier = String(describing: BeerCell.self) let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! BeerCell let beer = beers[indexPath.row] let url = imageUrl(for: beer) Nuke.Manager.shared.loadImage(with: url, into: cell.iconImageView) // ... } The imageUrl(for:) method is used to provide a stand-in image for beer records that don't have one. It uses placeimg.com for this. private func imageUrl(for beer: Beer) -> URL { return beer.iconImageUrl ?? URL(string: "https://placeimg.com/300/300/beer?id=\(beer.id)")! } It is important that we modify the URL in such a way that each beer has a deterministic, but unique URL. If we use the same URL for multiple beers, Nuke will cache the first one and use it for all of the other requests. Preheating Image Requests In our prefetchDatasource callback methods, we can determine which URLs we need to load and tell Nuke to preheat those images. These requests get fired at a lower-priority, but will often finish loading (and be cached) before this cell is displayed. First we need a method that will transform a set of index paths into a set of image urls (provided the beers array has an entry already for this row): func imageRequests(indexPaths: [IndexPath]) -> [Nuke.Request] { let beers: [Beer] = indexPaths.flatMap { indexPath in guard indexPath.row < self.beers.count else { return nil } return self.beers[indexPath.row] } let imageUrls = beers.map(self.imageUrl) let requests = imageUrls.map(Nuke.Request.init) return requests } Then we can pass those off to a preheater, which is an object we need to instantiate and keep track of. Add a property at the top of the class: let preheater = Nuke.Preheater() Then we can use it when we are told to prefetch rows: func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) { let requests = imageRequests(indexPaths: indexPaths) preheater.startPreheating(with: requests) // ... } When the user stops scrolling or changes direction, we'll see cancellations come in. We can pass these off and tell Nuke to stop preheating those requests. This is important because those requests are no longer the most important ones. func tableView(_ tableView: UITableView, cancelPrefetchingForRowsAt indexPaths: [IndexPath]) { let requests = imageRequests(indexPaths: indexPaths) preheater.stopPreheating(with: requests) } With this in place our images will seamlessly load, either by preheating, or by loading directly when the cell is requested. Nuke handles the case where a request is still in progress when an image is requested, and associates the image view with that request.