I'm using UICollectionView
to build UI that can display elements in a grid or a vertical list layout. UICollectionViewFlowLayout
doesn't play well with a full-width list layout, so I'm writing my own UICollectionViewLayout
subclass. And the rows are self-sizing, so that they can have multiline labels and match their font size to the system font size setting and grow/shrink as needed
Internally, it has a collection of layout attributes for all rows. In prepareLayout
I walk over all index paths in the data source and create layout attributes with their frames set for an estimated row height.
In shouldInvalidateLayoutForPreferredLayoutAttributes
, I return YES
if the height is different than the value I have for that row in my collection. Then in invalidationContextForPreferredLayoutAttributes
, I invalidate all items after the index path of the passed-in preferredAttributes
, because a height change in this row affects the vertical position of all subsequent rows. Then prepareLayout
gets called again and I update the frames of everything as necessary.
The problem comes with this sequence of events:
layoutAttributesForElementsInRect
gets called. I walk over all my attributes and check ifCGRectIntersectsRect(attr.frame, rect)
.shouldInvalidateLayoutForPreferredLayoutAttributes
gets called for each row that is about to appear. Some of them end up smaller than the estimated height they had originally.
If the total row shrinkage is enough, a row or two that weren't within the rect for layoutAttributesForElementsInRect
get moved up enough that they should appear. For example, if the rect was y values 0 through 1334, a row with y origin 1340 before the calls to shouldInvalidateLayoutForPreferredLayoutAttributes
could now be at 1300. But the layout object already "knows" what rows are supposed to be onscreen, so those rows just don't show up, and there are holes in my list.
I might be able to work around this by returning extra rows from layoutAttributesForElementsInRect
(by expanding the rect I get by 100 points vertically or something). But that's a hack, and this seems like something that the APIs should have a way to address. But I don't get another call to layoutAttributesForElementsInRect
after self-sizing is done, and I don't see a way to ask for it to be called again via UICollectionViewLayoutInvalidationContext
.
So...am I missing something obvious in the API? Is this a problem that shouldn't be happening, which means I'm approaching something wrong? It seems like there's no way to accurately answer layoutAttributesForElementsInRect
before self-sizing has happened...?
Example code illustrating the problem is here on GitHub.
I've discovered that having extra invalidations in
invalidationContextForPreferredLayoutAttributes
when you have self-sizing supplementary views will cause a bug where you get layout gaps. I've made the same mistake and others have too because it seems logical to invalidate the items below and item being self-sized. My guess is that the collection view handles this for you.These are the lines to remove in your sample project:
I've filed a documentation update Radar with Apple about this. http://www.openradar.me/35833995