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 (prepare in Swift) 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:
layoutAttributesForElementsInRectgets called. I walk over all my attributes and check ifCGRectIntersectsRect(attr.frame, rect).shouldInvalidateLayoutForPreferredLayoutAttributesgets 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.