2

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:

  • layoutAttributesForElementsInRect gets called. I walk over all my attributes and check if CGRectIntersectsRect(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.

qtmfld
  • 2,916
  • 2
  • 21
  • 36
Tom Hamming
  • 10,577
  • 11
  • 71
  • 145

2 Answers2

2

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:

[result invalidateItemsAtIndexPaths:rowPaths];
[result invalidateSupplementaryElementsOfKind:UICollectionElementKindSectionHeader atIndexPaths:headerPaths];

I've filed a documentation update Radar with Apple about this. http://www.openradar.me/35833995

brynbodayle
  • 6,546
  • 2
  • 33
  • 49
  • On my case where I have a fix item size but self sizing reusable views, I encountered crash issues on ios 11 and 12 caused by internal workings of collectionview invalidationLayout(context) via super call. To resolve it, ios 12 below i dont call super and assemble the context manually. – Teffi Jul 21 '21 at 14:50
0

My proble was that layout attributes that I was returning from my custom layout were missing zIndex property so self-sizing did not work properly.

olejnjak
  • 1,163
  • 1
  • 9
  • 23