UICollectionView Tutorial: Prefetching APIs

In this UICollectionView prefetching tutorial, you’ll learn how to achieve smooth scrolling in your app using Operations and Prefetch APIs. By Christine Abernathy.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 2 of this article. Click here to view the first page.

Enabling UICollectionView Prefetching

The UICollectionViewDataSourcePrefetching protocol gives you advance warning that data for a collection view could be needed soon. You can use this information to begin prefetching data so that, when the cell is visible, the data may already be available. This works in conjunction with the concurrency work you've already done — the key difference being when the work gets kicked off.

The diagram below shows how this plays out. The user is scrolling upwards on a collection view. The yellow cell should be coming into view, soon — assume this happens in Frame 3 and you're currently in Frame 1.

prefetch-steps

Adopting the prefetch protocol informs the app about the next cells that may become visible. Without the prefetch trigger, the data fetch for the yellow cell starts in Frame 3 and the cell's data becomes visible some time later. Due to the prefetch, the cell data will be ready by the time the cell is visible.

love-it

Open EmojiViewController.swift and add the following code to the end of the file:

// MARK: - UICollectionViewDataSourcePrefetching
extension EmojiViewController: UICollectionViewDataSourcePrefetching {
  func collectionView(_ collectionView: UICollectionView,
      prefetchItemsAt indexPaths: [IndexPath]) {
    print("Prefetch: \(indexPaths)")
  }
}

EmojiViewController now adopts the UICollectionViewDataSourcePrefetching and implements the required delegate method. The implementation simply prints out the index paths that could be visible soon.

In viewDidLoad(), add the following after the call to super.viewDidLoad():

collectionView?.prefetchDataSource = self

This sets EmojiViewController as the collection view's prefetching data source.

Build and run the app and, before scrolling, check Xcode's console. You should see something like this:

Prefetch: [[0, 10], [0, 11], [0, 12], [0, 13], [0, 14], [0, 15]]

These correspond to cells that are yet to become visible. Now, scroll more and check the console log as you do. You should see log messages based on index paths that are not visible, yet. Try scrolling both upwards and downwards until you get a good sense of how this all works.

You might be wondering why this delegate method simply gives you index paths to work with. The idea is that you should kick off your data-loading processes from this method, then handle the results in collectionView(_:cellForItemAt:) or collectionView(_:willDisplay:forItemAt:). Note that the delegate method is not called when cells are required immediately. You should, therefore, not rely on loading data into the cells in this method.

Prefetching Data Asynchronously

In EmojiViewController.swift, modify collectionView(_:prefetchItemsAt:) by replacing the print() statement with the following:

for indexPath in indexPaths {
  // 1
  if let _ = loadingOperations[indexPath] {
    continue
  }
  // 2
  if let dataLoader = dataStore.loadEmojiRating(at: indexPath.item) {
    // 3
    loadingQueue.addOperation(dataLoader)
    loadingOperations[indexPath] = dataLoader
  }
}

The code loops through the index paths the method receives and does the following:

  1. Checks if there's an existing loading operation for this cell. If there is, there's nothing more to do.
  2. Creates a data-loading operation if it doesn't find a loading operation.
  3. Adds the operation to the queue and updates the dictionary that tracks data loading operations.

The index paths passed into collectionView(_:prefetchItemsAt:) are ordered by priority, based on the cells geometric distance to the Collection View's view. This allows you to fetch the cells you'll most likely need first.

Recall that you had previously added code in collectionView(_:willDisplay:forItemAt:) to handle the results of the loading operation. Look at the highlights from that method below:

override func collectionView(_ collectionView: UICollectionView,
    willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  // ...
  let updateCellClosure: (EmojiRating?) -> Void = { [weak self] emojiRating in
    guard let self = self else {
      return
    }
    cell.updateAppearanceFor(emojiRating, animated: true)
    self.loadingOperations.removeValue(forKey: indexPath)
  }
  
  if let dataLoader = loadingOperations[indexPath] {
    if let emojiRating = dataLoader.emojiRating {
      cell.updateAppearanceFor(emojiRating, animated: false)
      loadingOperations.removeValue(forKey: indexPath)
    } else {
      dataLoader.loadingCompleteHandler = updateCellClosure
    }
  } else {
    // ...
  }  
}

After creating the cell update closure, you check the array that tracks operations. If it exists for the cell that's about to appear and the emoji is available, the cell's UI is updated. Note that the closure passed to the data loading operation also updates the cell's UI.

This is how everything ties in, from prefetch triggering an operation to the cell's UI being updated.

Build and run the app and scroll through the emojis. Emojis you scroll to should be visible much faster than before.

UICollectionView prefetching in action!

Pop-quiz time! Can you spot something that could be improved? No cheating by looking at the next section title. Well, if you scroll really fast, then your collection view will start fetching emojis that may never be seen. What's a obsessive programmer to do? Read on.

Canceling a Prefetch

UICollectionViewDataSourcePrefetching has an optional delegate method that lets you know that data is no longer required. This may happen because the user has started scrolling really fast and intermediate cells are likely not to be seen. You can use the delegate method to cancel any pending data-loading operations.

Still in EmojiViewController.swift, add the following method to your UICollectionViewDataSourcePrefetching protocol implementation:

func collectionView(_ collectionView: UICollectionView,
  cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
  for indexPath in indexPaths {
    if let dataLoader = loadingOperations[indexPath] {
      dataLoader.cancel()
      loadingOperations.removeValue(forKey: indexPath)
    }
  }
}

The code loops through the index paths and finds any loading operations attached to them. It then cancels the operation and removes it from the dictionary that tracks operations.

Build and run the app. As you scroll really quickly, operations that may have started should start to cancel. Visually, things won't look much different.

One thing to note is that, due to cell reuse, some previously visible cells may need to be re-fetched. Don't panic if you see the loading indicator on those puppies — uhm, emojis.

Where to Go From Here?

Congratulations! You've successfully turned a sluggish app into a verifiable speedster.

You can download the final project using the Download Materials button at the top or bottom of this tutorial.

This tutorial discussed collection views, but there's a similar prefetch API available for table views. You can see an example of how this is used in the UITableView Infinite Scrolling Tutorial.

In an app with latency loading data, smooth scrolling isn't possible without concurrency. Be sure to check out the Operation and OperationQueue Tutorial for a deeper dive on this topic.

Add these to your grab bag of tricks to make your app more responsive. It's these little things that add up and go a long way to keeping your users delighted. Happy scrolling!

I do hope you've enjoyed this tutorial. If you have any comments or questions about the tutorial, please join the forum discussion below!