4

Say you have a UICollectionView with a normal custom UICollectionViewLayout.

So that is >>> NOT <<< a flow layout - it's a normal custom layout.

Custom layouts are trivial, in the prepare call you simply walk down the data and lay out each rectangle. So say it's a vertical scrolling collection...

override func prepare() {
    cache = []
    var y: CGFloat = 0
    let k = collectionView?.numberOfItems(inSection: 0) ?? 0
    // or indeed, just get that direct from your data
    
    for i in 0 ..< k {
        
        // say you have three cell types ...
        let h = ... depending on the cell type, say 100, 200 or 300
        
        let f = CGRect(
            origin: CGPoint(x: 0, y: y ),
            size: CGSize(width: screen width, height: h)
        )
        
        y += thatHeight
        y += your gap between cells
        
        cache.append( .. that one)
    }
}

In the example the cell height is just fixed for each of the say three cell types - all no problem.

Handling dynamic cell heights if you are using a flow layout is well-explored and indeed relatively simple. (Example, also see many explanations on the www.)

However, what if you want dynamic cell heights with a (NON-flow) completely normal everyday UICollectionViewLayout?

Where's the estimatedItemSize ?

As far as I can tell, there is NO estimatedItemSize concept in UICollectionViewLayout?

So what the heck do you do?

You could naively just - in the code above - simply calculate the final heights of each cell one way or the other (so for example calculating the height of any text blocks, etc). But that seems perfectly inefficient: nothing at all of the collection view, can be drawn, until the entire 100s of cell sizes are calculated. You would not at all be using any of iOS's dynamic heights power and nothing would be just-in-time.

I guess, you could program an entire just-in-time system from scratch. (So, something like .. make the table size actually only 1, calculate manually that height, send it along to the collection view; calculate item 2 height, send that along, and so on.) But that's pretty lame.

Is there any way to achieve dynamic height cells with a custom UICollectionViewLayout - NOT a flow layout?

(Again, of course obviously you could just do it manually, so in the code above calculate all at once all 1000 heights, and you're done, but that would be pretty lame.)

Like I say above the first puzzle is, where the hell is the "estimated size" concept in (normal, non-flow) UICollectionViewLayout?

Community
  • 1
  • 1
Fattie
  • 30,632
  • 54
  • 336
  • 607

1 Answers1

3

Just a warning: custom layouts are FAR from trivial, they may deserve a research paper on their own ;)

You can implement size estimation and dynamic sizing in your own layouts. Actually, estimated sizes are nothing special; rather, dynamic sizes are. Because custom layouts give you a total control of everything, however, this involves many steps. You will need to implement three methods in your layout subclass and one method in your cells.

  1. First, you need to implement preferredLayoutAttributesFitting(_:) in your cells (or, more generally, reusable views subclass). Here you can use whatever calculations you want. Chances are that you will use auto layout with your cells: if so, you will need to add all cell's subviews to its contentView, constrain them to the edges and then call systemLayoutSizeFitting(_:withHorizontalFittingPriority:verticalFittingPriority:) within this "preferred attributes" method. For example, if you want your cell to resize vertically, while being constrained horizontally, you would write:

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    
                // Ensures that cell expands horizontally while adjusting itself vertically.
                let preferredSize = systemLayoutSizeFitting(layoutAttributes.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
    
                layoutAttributes.size = preferredSize
                return layoutAttributes
            }
    
  2. After the cell is asked for its preferred attributes, the shouldInvalidateLayout(forPreferredLayoutAttributes:withOriginalAttributes:) on the layout object will be called. What's important, you can't just simply type return true, since the system will reask the cell indefinitely. This is actually very clever, since many cells may react to each other's changes, so it's the layout who ultimately decides if it's done satisfying the cells' wishes. Usually, for resizing, you would write something like this:

    override func shouldInvalidateLayout(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> Bool {
    
                if preferredAttributes.size.height.rounded() != originalAttributes.size.height.rounded() {
                    return true
                }
                return false
            }
    
  3. Just after that, invalidationContext(forPreferredLayoutAttributes:withOriginalAttributes:) will be called. You usually would want to customize the context class to store the information specific to your layout. One important, rather unintuitive, caveat though is that you should not call context.invalidateItems(at:) because this will cause the layout to invalidate only those items among the provided index paths that are actually visible. Just skip this method, so the layout will requery the visible rectangle.

    However! You need to thoroughly think if you need to set contentOffsetAdjustment and contentSizeAdjustment: if something resizes, your collection view as a whole probably will shrink or expand. If you do not account for those, you will have jump-reloads when scrolling.

  4. Lastly, invalidateLayout(with:) will be called. This is the step that's intended for you to actually adjust your sections/rows heights, move something that's been affected by the resizing cell etc. If you override, you will need to call super.

PS: This is really a hard topic, I just scratched the surface. You can look here how complicated it gets (but this repo is also a very rich learning tool).

halfer
  • 18,701
  • 13
  • 79
  • 158
WTEDST
  • 870
  • 3
  • 15