Making a Complex UICollectionView Layout With Self Sizing Cells (Part 3)

Maximilian Clarke
5 min readAug 2, 2020

--

“New York” by Michel MARIE is licensed under CC BY-NC-ND 2.0

In Part 2 I took you through some of the potential solutions to common pain points with self sizing cells in a custom layout. Now we have a working layout, we can optimise it for silky smooth scrolling performance!

The Problem

While the user is scrolling, items move into view, and the self sizing dance plays out, culminating in invalidations and calls to prepare. That’s not good — prepare has been our safe haven for expensive calculations for the whole layout attributes cache, and it’s now being called during scroll! So now we need to optimise, to ensure only the minimal set of calculations are performed to cater for the new item size.

For the rest of this post I’ll use a very simple vertically scrolling table view layout by way of example, so we can understand the mechanics of how the process works without unnecessary complexity.

What should be recalculated?

So, let’s say a cell in our table view layout responds with preferred attributes that contain a larger size than the original attributes. Since it’s a table view cell, the larger size can only mean a larger height. This larger height will require us to push all the cells underneath it down to make room.

So we need to:

  • Adjust the height for this views attributes
  • Adjust the origin of all the attributes for views underneath this one to make room for the new height
  • Adjust the contentSize of the layout to also make room for the new height

That is the bare minimum amount of work needed. How do we do only that and avoid a full recalculation?

Invalidation Contexts

These are the path to stutter free scrolling. They’re basically a description of what is now considered invalid or “dirty”, so that the collection view can know what parts of the layout it needs to ask for again (rather than the whole thing again). But also they are a description of what we need to recalculate, so when the collection view asks for attributes again we can give it correctly updated ones.

In the lovely diagram from part 1, the self sizing process is outlined. There are a few steps that are pertinent to invalidation:

  • shouldInvalidateLayout(forPreferredAttributes:withOriginalAttributes:)
  • invalidationContext(forPreferredLayoutAttributes:withOriginalAttributes:)
  • invalidateLayout(with:)
  • prepare()

In shouldInvalidateLayout, we are given an opportunity to compare the preferred and original attributes to decide if an invalidation is even necessary. If the new item scrolling into view returned the same height as the original attributes we provided, then no invalidation is needed. If the height is different, the process continues. In our example above, the height is larger, so we would return true here.

Next, in invaldationContext, we’re asked to configure an invalidation context that represents what is now invalid. Following the advice in part 1, we specify the index paths that are now invalid, and the adjustment to contentSize that is required for the invalidation. With the height being larger, we invalidate all the paths including and below this one, and specify positive adjustment to contentSize height, which is the difference between the original and preferred height. Remember, positive values indicate a growth in size, negative values shrink. We also should remember the preferred height for this index path here, so it can be used in prepare.

In invalidateLayout, we’re currently not doing anything, and just waiting for prepare.

In prepare we’re doing a full recalculation of the entire layout.

What if I told you we could simply adjust our cached attributes in theinvalidateLayout step and exit early in prepare so the full recalculation isn’t performed? Wouldn’t that be great? One small hitch though: we don’t have enough information in the context for invalidateLayout to be used for this purpose. We know what paths are now invalid, but we don’t know what path needs height adjustment*.

This is where a subclass of UICollectionViewLayoutInvalidationContext comes to the rescue. Just like we can subclass UICollectionViewLayoutAttributes to provide more attributes data for the layout to use, we can subclass UICollectionViewLayoutInvalidationContext to provide more data for invalidations to use. So we can put whatever information we want in there to guide the layout adjustment in invalidateLayout, like the path whose height will need adjusting 🙌

Subclassing UICollectionViewLayoutInvalidationContext

First let’s create a subclass like:

class TableViewLayoutInvalidationContext: UICollectionViewLayoutInvalidationContext {
var heightAdjustedIndexPath: IndexPath?
}

And as with UICollectionViewLayoutAttributes we need to tell our layout about the subclass, so it knows to create one of these in calls to super.invalidationContext(forPreferredLayoutAttributes:withOriginalAttributes:):

class TableViewLayout: UICollectionViewLayout {  override class var invalidationContextClass: AnyClass { TableViewLayoutInvalidationContext.self }  ...
}

Now inside invalidateLayoutwe have everything we need to adjust only the parts of the layout that need adjusting (don’t forget to cast context to your subclass type):

  • Adjust the height of the attributes for the self sizing path, using context.heightAdjustedIndexPath and context.contentSizeAdjustment.height
  • Iterate over the attributes for the paths underneath it to adjust the origin y value to make room for the new size, using context.invalidatedItemIndexPaths and context.contentSizeAdjustment.height
  • Adjust the contentSize , using context.contentSizeAdjustment

Much cheaper! Now the layout simply adjusts rather than totally recalculates.

Also, we no longer need to remember any preferred heights for use in prepare, because only full recalculations should occur in prepare now (remember that a full recalculation will end up with the self sizing dance for each item as well). That some extra state we no longer need to worry about.

More Complex Layouts

Obviously our table view layout example is over simplified. For your complex layout, you will need to get creative. I’d recommend cutting up some bits of paper to represent all your cells, and laying them out on a surface in front of you. Then, swap one of them to a new size. Adjust the layout with your hands and see what parts need to be moved around. Adjusting the size of one of the items might have significant knock on effects to some others. It might cause items to move to new rows or columns for example.

Do this for all the variations you can think of, and you should get a feel for what needs to be adjusted in what section when different cells self size for dynamic content. Then, think about what information you will need in the UICollectionViewLayoutInvalidationContext subclass, and what the algorithm/s that use this information to adjust the calculated attributes will look like. Every layout is a bit different. Good luck!

End of part 3

And this concludes the series on complex UICollectionViewLayouts with self sizing cells. We learnt about the layout process as a whole, how self sizing affects it, common pitfalls and their solutions, and finally how to optimise your layout with invalidation contexts. I hope it helped you with your complex layout implementation! Until next time 👋

*Well, in truth we do know what path needs height adjustment (it’s always the first in the array of invalid paths), but for the sake of example we’ll pretend we don’t

--

--

Maximilian Clarke

Technical lead at Tigerspike, synth enthusiast, husband, not necessarily in that order 😜