Collection View updates in iOS10, part 1

June 25, 2016

Following WWDC, iOS 10 is now out in beta form ahead of a probable release in the autumn. There are a whole load of brand-new features, but also some tweaks to existing ones, and UICollectionView is no exception.

Although UICollectionView is a highly-performant control, Apple haven’t let it stand still. One of the most interesting new features is designed to improve the performance of collection views with expensive data sources.

##Background The aim for any iOS application is to run the UI at the full 60 frames per second, to deliver a completely smooth scrolling performance. Any frame rate significantly lower than this will manifest itself by dropped frames and stuttery performance.

60fps equates to 16.67ms per frame, which isn’t long if you’re dealing with drawing collection view cells using data from slow sources. The trick to high performance collecion views is to get the cellForItemAtIndexPath to return a cell as fast as possible, but this can be difficult. Techniques like asynchronous fetching of images can help, but these aren’t a miracle cure.

Note: This technique is also available to use with UITableView - just replace references to UICollectionView with UITableView, and the implementation patterns are identical.

##Prefetching

In iOS10, Apple have introduced a new UICollectionViewDataSource protocol extension called UICollectionViewDataSourcePrefetching.

This introduces a new property on UICollectionView called prefetchDataSource. This is a class that implements two UICollectionViewDataSourcePrefetching protocol functions:

  • collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath])

  • collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath])

The prefetchItemsAt indexPaths: function is called by the collection view when it looks like the scrolling rate of the collection view will outstrip the ability of cellForItemAtIndexPath to deliver cells in a timely manner.

The collection view passes in an Array of NSIndexPaths for cells that are likely to be needed in the future. This gives you an opportunity to update the data source that underlies the collection view. For example, if your data source was an Array of images, you could call out to the network to download the images and insert them into the data source so that they are ready to be used by the cellForRowAtIndexPath function.

The second method is normally called when the scrolling direction changes. The reasoning for this is that you’re likely to be prefetching data well in advance of the cells being displayed - if those cells won’t now get displayed because the collection view is scrolling away from them, you may as well cancel whatever operation it is that you’re carrying out to update the data source. Again, the index paths of the cells in question are passed in as an Array of NSIndexPaths.

##Implementing pre-fetching

To take advantage of the new prefetch feature, you’ll need to do four things:

  • Conform a class to the UICollectionViewDataSourcePrefetching protocol
  • Implement the collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) function to update the collection view’s dataSource
  • Optionally, implement the collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) function to cancel any in-flight prefetching operations
  • Set the conforming class as the collectionView’s prefetchDataSource property.

Conforming to UICollectionViewDataSourcePrefetching

This isn’t difficult: just mark your data source as implementing the protocol:

class MyViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDataSourcePrefetching {
	...
}

Implementing prefetching

Prefetching is implementing with the collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) function. There the collection view passes in an Array of NSIndexPaths, one for each of the cells that are likely to be displayed.

Your job here is to iterate across this array, and update the collection view’s data source accordingly:

func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {

    for indexPath in indexPaths {
		
		... expensive operation to retrieve some data...
		
        dataArray[indexPath.row] = retrievedData
		
    }
    
}

There are a couple of important points to note here: the first is that you’re only updating the collection view’s underlying data source - not returning any data directly. This could be as simple as updating an Array of strings (as above), or more complex like updating a CoreData or Realm model.

The second point to bear in mind is that after the prefetch has taken place, there’s no guarantee when, or if, that retrieved will be used. The collection view’s scroll rate could slow down; or the direction reverse entirely.

For this reason, there is a chance that time-critical data could be stale by the time it’s displayed. Whether this is a factor that you need to consider will of course depend on the nature of the data being displayed.

Implenting prefetch cancellation

To an extent, requests for prefetches are a bit of a guess - the collection view is attempting to optimise for an uncertain future state that might not actually come to pass. For example, if the scroll rate slows, or reverses altogether, there’s a chance that the cells for which prefetch has been requested may never actually be displayed.

In this situation, any inflight prefetch would be wasted effort. Rather than make redundant requests, the UICollectionViewDataSourcePrefetch protocol defines a function to cancel outstanding requests: collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath])

This is called if the collection view decides that prefetching would be over-optimisation - it passes an Array of NSIndexPaths as the function’s parameter, and it’s up to you to iterate over those and cancel any requests where it makes sense to do so.

Hooking up the prefetch delegate to the collection view

With the class conforming to the protocol, and with one or both of the functions implemented, you need to add it as a prefetchDataSource to the collection view.

This is as simple as updating the property (here, we’re assuming that the collection view controller is acting as its own prefetchDataSource):

collectionView.prefetchDataSource = self

This has to be done before any calls to any of the UICollectionViewDataSource protocol functions, so it probably makes sense to do so at the same time as setting up the collection view’s other properties, such as cell registration.

##Conclusion UICollectionView is a high-performance control to begin with, with a significant amount of ‘under the hood’ optimisation going on that we don’t see. Prefetching is another tool to eke out even more performance, especially in situations where getting the source data for display in collection view cells is expensive or slow.

However, there’s a saying that “premature optimisation is the root of all evil” in code, so it’s not a magic bullet. And don’t overlook the fact that often the most expensive part of the process is compositing the collection view cell itself. But if you’re looking to squeeze out the last drops of performance to make your collection views as smooth as possible, prefetching may be worth a try.