Making a Complex UICollectionView Layout With Self Sizing Cells (Part 3)
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 invalidateLayout
we 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
andcontext.contentSizeAdjustment.height
- Iterate over the attributes for the paths underneath it to adjust the origin
y
value to make room for the new size, usingcontext.invalidatedItemIndexPaths
andcontext.contentSizeAdjustment.height
- Adjust the
contentSize
, usingcontext.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 UICollectionViewLayout
s 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