Smooth Scrolling in UITableView and UICollectionView
As most iOS developers know, displaying sets of data is a rather common task in building a mobile app. Apple’s SDK provides two components to help carry out such a task without having to implement everything from scratch: A table view (UITableView) and a collection view (UICollectionView).
Table views and collection views are both designed to support displaying sets of data that can be scrolled. However, when displaying a very large amount of data, it could be very tricky to achieve a perfectly smooth scrolling. This is not ideal because it negatively affects the user experience.
As a member of the iOS dev team for the Capital One Mobile app, I’ve had the chance to experiment with table views and collection views; this post reflects my personal experience in displaying large amounts of scrollable data. In it, we’ll review the most important tips to optimize the performance of the above mentioned SDK components. This step is paramount to achieving a very smooth scrolling experience. Note that most of the following points apply to both UITableView and UICollectionView as they share a good amount of their “under the hood” behavior. A few points are specific to UICollectionView, as this view puts additional layout details on the shoulders of the developer.
Let’s begin with a quick overview of the above mentioned components.
UITableView is optimized to show views as a sequence of rows. Since the layout is predefined, the SDK component takes care of most of the layout and provides delegates that are mostly focused on displaying cell content.
UICollectionView, on the other hand, provides maximum flexibility as the layout is fully customizable. However, flexibility in a collection view comes at the cost of having to take care of additional details regarding how the layout needs to be performed.
Tips Common to both UITableView and UICollectionView
NOTE: I am going to use UITableView for my code snippets. But the same concepts apply to UICollectionView as well.
Cells Rendering is a Critical Task
The main interaction between UITableView and UITableViewCell can be described by the following events:
- The table view is requesting the cell that needs to be displayed (tableView(_:cellForRowAt:)).
- The table view is about to display the cell (tableView(_:willDisplay:forRowAt:)).
- The cell has been removed from the table view (tableView(_:didEndDisplaying:forRowAt:)).
For all the above events, the table view is passing the index (row) for which the interaction is taking place. Here’s a visualization of the UITableViewCell lifecycle:
First off, the tableView(_:cellForRowAt:) method should be as fast as possible. This method is called every time a cell needs to be displayed. The faster it executes, the smoother scrolling the table view will be.
There are a few things we can do in order to make sure we render the cell as fast as possible. The following is the basic code to render a cell, taken from Apple’s documentation:
After fetching the cell instance that is about to be reused (dequeueReusableCell(withIdentifier:for:)), we need to configure it by assigning the required values to its properties. Let’s take a look at how we can make our code execute quickly.
Define the View Model for the Cells
One way is to have all the properties we need to show be readily available and just assign those to the proper cell counterpart. In order to achieve this, we can take advantage of the MVVM pattern. Let’s assume we need to display a set of users in our table view. We could define the Model for the User as:
Defining a View Model for the User is straightforward:
Fetch Data Asynchronously and Cache View Models
Now that we have defined our Model and View Model, let’s get them to work! We are going to fetch the data for the users through a web service. Of course, we want to implement the best user experience possible. Therefore, we will take care of the following:
- Avoid blocking the main thread while fetching data.
- Updating the table view right after we retrieve the data.
This means we will be fetching the data asynchronously. We will perform this task through a specific controller, in order to keep the fetching logic separated from both the Model and the View Model, as follows:
Now we can retrieve the data and update the table view asynchronously as shown in the following code snippet:
We can use the above snippet to fetch the users data in a few different ways:
- Only the when loading the table view the first time, by placing it in viewDidLoad().
- Every time the table view is displayed, by placing it in viewWillAppear(_:).
- On user demand (for instance via a pull-down-to-refresh), by placing it in the method call that will take care of refreshing the data.
The choice depends on how often the data can be changing on the backend. If the data is mostly static or not changing often the first option is better. Otherwise, we should opt for the second one.
Load Images Asynchronously and Cache Them
It’s very common to have to load images for our cells. Since we’re trying to get the best scrolling performance possible, we definitely don’t want to block the main thread to fetch the images. A simple way to avoid that is to load images asynchronously by creating a simple wrapper around URLSession:
This lets us fetch each image using a background thread and then update the UI once the required data is available. We can improve our performances even further by caching the images.
In case we don’t want - or can’t afford - to write custom asynchronous image downloading and caching ourselves, we can take advantage of libraries such as SDWebImage or AlamofireImage. These libraries provide the functionality we’re looking for out-of-the-box.
Customize the Cell
In order to fully take advantage of the cached View Models, we can customize the User cell by subclassing it (from UITableViewCell for table views and from UICollectionViewCell for collection views). The basic approach is to create one outlet for each property of the Model that needs to be shown and initialize it from the View Model:
Use Opaque Layers and Avoid Gradients
Since using a transparent layer or applying a gradient requires a good amount of computation, if possible, we should avoid using them to improve scrolling performance. In particular, we should avoid changing the alpha value and preferably use a standard RGB color (avoid UIColor.clear) for the cell and any image it contains:
Putting Everything Together: Optimized Cell Rendering
At this point, configuring the cell once it’s time to render it should be easy peasy and really fast because:
- We are using the cached View Model data.
- We are fetching the images asynchronously.
Here’s the updated code:
Tips Specific to UITableView
Use Self-Sizing Cells for Cells of Variable Height
In case the cells we want to display in our table view have variable height, we can use self sizable cells. Basically, we should create appropriate Auto Layout constraints to make sure the UI components that have variable height will stretch correctly. Then we just need to initialize the estimatedRowHeight and rowHeight property:
NOTE: In the unfortunate case we can’t use self-sizing cells (for instance, if support for iOS7 is still required) we’d have to implement tableView(_:heightForRowAt:) to calculate each cell height. It is still possible, though, to improve scrolling performances by:
- Pre-calculating all the row heights at once.
- Return the cached value when tableView(_:heightForRowAt:) is called.
Tips Specific to UICollectionView
We can easily customize most of our collection view by implementing the appropriate UICollectionViewFlowLayoutDelegate protocol method.
Calculate your Cell Size
We can customize our collection view cell size by implementing collectionView(_:layout:sizeForItemAt:):
Handle Size Classes and Orientation Changes
We should make sure to correctly refresh the collection view layout when:
- Transitioning to a different Size Class.
- Rotating the device.
This can be achieved by implementing viewWillTransition(to:with:):
Dynamically Adjust Cell Layout
In case we need to dynamically adjust the cell layout, we should take care of that by overriding apply(_:) in our custom collection view cell (which is a subclass of UICollectionViewCell):
For instance, one of the common tasks usually performed inside this method is adjusting the maximum width of a multi-line UILabel, by programmatically setting its preferredMaxLayoutWidth property:
You can find a small sample with the proposed tips forUITableView and UICollectionView here.
In this post we examined some common tips to achieve smooth scrolling for both UITableView and UICollectionView. We also presented some specific tips that apply to each specific collection type. Depending on the specific UI requirements, there could be better or different ways to optimize your collection type. However, the basic principles described in this post still apply. And, as usual, the best way to find out which optimizations work best is to profile your app.