Here at Spring, we recently embraced Auto Layout in all of its glory to create dynamically sized cells for our collection views. (We then wrote a blog post that I will shamelessly promote here.) However, after the honeymoon period was over, we began to notice a few problems:

Scrolling was occasionally choppy. Supplementary views were sometimes misplaced.

Ew.

In this article, we will talk about the results of our investigation into these issues, and the solution we employed to solve them. Our solution uses Apple’s traditional non-self-sizing layout framework and incorporates dynamically sized cells into its delegate methods.

Choppy Scrolling

We were able to reproduce the choppy scrolling with the following steps:

Scroll down in the collection view. Push another view controller onto the navigation stack. Pop the view controller so we are back on our collection view. Scroll up. The scrolling will be choppy.

The Cause

Somewhere between the calls to viewWillAppear: and viewDidAppear: on the collection view’s view controller, the collection view is throwing away the sizes it had previously computed for any cells not on screen. The collection view replaces those sizes with the estimatedItemSize and adjusts the contentOffset and contentSize of the collection view so that it appears as if nothing has changed.

Visualization of our collection view’s content at viewWillAppear: and viewDidAppear:. Off screen cells get resized to their estimatedItemSize.

However, when scrolling up, Auto Layout calculates the actual size for cells that are about to appear on screen, and replaces the estimated size with the actual size. This sudden change results in the collection view appearing to “jump” as you scroll up the screen.

What about UICollectionViewFlowLayoutEstimatedSize?

In iOS 10, Apple introduced an analog to UITableViewAutomaticDimension . The new constant is called UICollectionViewFlowLayoutEstimatedSize and you can return it from UICollectionViewFlowLayoutDelegate 's -[collectionView: layout: sizeForItemAtIndexPath:] method. If you return this constant, the system will dynamically update the estimatedItemSize as cell sizes are calculated. Check out WWDC 2016 Session 219 for more information and a cool visualization of how estimatedItemSize gets used.

Unfortunately, dynamically changing estimatedItemSize does not solve our problem, it only makes it less obvious. Every off-screen item is still using estimatedItemSize , and when that size has to be replaced by the actual size, the same choppy behavior will occur.

Misplaced Supplementary Views

We didn’t investigate this issue much ourselves, but according to this post from Stack Overflow:

Each time when auto layout code updates cell’s frame it does nothing with headers and footers.

The post suggests overriding a method on UICollectionViewFlowLayout to invalidate supplementary view layouts. This solution did not work for us, so we developed our own solution.

The Solution: Do It Yourself

Since Apple’s out-of-the-box implementation of self-sizing collection view cells was not working for us, we decided to do it ourselves (don’t worry, it is not as scary as it sounds). We basically use the old, “traditional” way of laying out collection views and simply incorporate self-sizing cells into it. I imagine this is how developers implemented self-sizing cells in UICollectionView prior to the introduction of estimatedItemSize in 2014.

At a high level, our implementation is as follows:

Use a sizing cell to compute the required size of a cell using Auto Layout. Cache that size in memory, and return it in UICollectionViewDelegateFlowLayout 's -[collectionView: layout: sizeForItemAtIndexPath:] method.

Step 1: Create a Sizing Cell

In order to save on instantiation costs, we recommend creating a singleton instance of your cell class to use as a sizing cell. This can be easily accomplished by adding the following method to your cell class:

Pro-Tip: If your cell uses a XIB, then you should replace sizingCell = [self new] with:

Step 2: Share Your Cell Configuration Logic

Usually we keep our cell configuration logic in -[collectionView: cellForItemAtIndexPath:] . Since this is where we are dequeuing the cell, it is a natural place for us to configure the cell prior to returning it.

However, in order to utilize our new sizing cell, we will need our cell configuration logic to be in a place that is accessible to both -[collectionView: cellForItemAtIndexPath:] and -[collectionView: layout: sizeForItemAtIndexPath:] .

In this example, we will assume all configuration is done in a view controller method called -[configureCell: atIndexPath:] .

Pro-Tip: If you call -[collectionView: cellForItemAtIndexPath:] from inside -[collectionView: layout: sizeForItemAtIndexPath:] , you’re gonna have a bad time. Like, infinite loop bad time.

Step 3: Compute the Required Size

Once the configuration logic is accessible, you can use your sizing cell to compute the size of your cell.

Be sure to use the same configuration logic when vending cells to the collection view, so that your computed size is consistent with what you’re showing on screen.

Step 4: Cache the Computed Size

Technically, Step 3 is as far as you need to go to solve the choppy scrolling and supplementary view problems we described. However, it is pretty bad for performance if your app has to recompute the size of your cells every time they are about to appear on screen.

To solve that performance issue, we go one step further and cache the computed sizes in memory. Simply create a dictionary, make a key that uniquely identifies your cell with respect to its size, and add your computed size to the dictionary. Then, return the cached value instead of re-computing the size of your cell again.

Pro-Tip: You can store a CGSize in a dictionary with +[NSValue valueWithCGSize:] . You can turn the NSValue object back into a CGSize with .CGSizeValue .

Pro-Tip: If the order and contents of your cells never change, indexPath will uniquely describe your cell, and you can use it as your key.

Step 5: ???

Drink some coffee.

Step 6: Profit

Go treat yourself to something nice and enjoy the buttery smooth scrolling of your dynamically sized collection view.

mmm…butter…

Shameless Plug

Spring is hiring.