How to: Compositional layout with self-sizing rows of N columns where the height of each item is set to the tallest item in its row

Paging Steve Breen! 😄

I've seen this question asked a zillion times but I've never seen an answer.

Is it possible to configure compositional layout to give you a grid of N columns (say 2 or 3) where each item in each row/group self-size their height, but the heights of those items are then set to be the height of the tallest item in their row.

This is easy to do if you ignore the self-sizing requirement (just use a fixed or absolute item height), but on the surface this doesn't even appear to be possible if you require self-sizing.

What I've Tried

  1. Configuring a layout where the items are set to a fractional height of 1.0 and their group is set to an estimated height (ex: 100). I was hoping compositional layout would interpret this as, "Please self-size the height of the group and make each item 100% of that height." Unfortunately, compositional layout just uses the estimate you provide for the height as the actual height and no self-sizing occurs at all. Sad panda. 🐼

  2. Use visibleItemsInvalidationHandler to attempt to identify which items share a row and reset their heights to be the height of the tallest item in that row. Sadly, the handler doesn't really have access to the data it needs to do this, nor is it allowed to change frames, nor is it called on the first layout pass, nor does it appear to be supported at all for layouts containing estimated items. In fact, if you try to use it with layouts that support self-sizing, you'll get this error message:

[UICompositionalLayout] note: the section invalidation handler cannot currently mutate visible items for layouts containing estimated items. Please file an enhancement request on UICollectionView.

  1. Wishing upon a star. Oh, and I also filed a radar asking: FB11776888

Here's a visual aid:

I have a little test program as well, but unfortunately I can't upload it here. Happy to share if it would be helpful. Here are a couple snippets:

#1

    // This doesn't work
    private func makeLayout1() -> UICollectionViewCompositionalLayout {

        // Item
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))

        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        // Group
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 2)
        group.interItemSpacing = .fixed(10)

        // Section
        let section = NSCollectionLayoutSection(group: group)
        return UICollectionViewCompositionalLayout(section: section)
    }

#2

    // This self-sizes the heights of the items, but allows the items in each row to be different heights rather than providing any way to constrain them to the height of the tallest self-sized item in each row
    private func makeLayout2() -> UICollectionViewCompositionalLayout {
        // Item
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .estimated(100))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        // Group
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, repeatingSubitem: item, count: 2)
        group.interItemSpacing = .fixed(10)

        // Section
        let section = NSCollectionLayoutSection(group: group)

        return UICollectionViewCompositionalLayout(section: section)
    }

Options?

My guess is that compositional layout simply doesn't support layouts that require a "partial second pass" per group, where the frames of the items can be updated based on information collected during self-sizing the other items in the group (to, for example, force them all to a common height, or align them top/bottom/centered within their group).

If that's the case (not supported), I would love a suggestion for where I might override the layout to provide this capability.

Thank you! ❤️

I feel weird replying to my own post, but since no one else has replied, I'm going to seed this with another example of why developers want this.

While my above example uses a two column grid, another common use case is a collection view containing a section with an orthogonal scroll direction, and a series of items with potentially different heights (self-sizing for the win, right?). Think: Horizontally scrolling carousel of cards. It's easy to force the height of this "carousel" and its items to some fixed height, but we don't want that. We want the content to self-size and the height to be a function of that. That's also easy if we don't care about the vertical alignment of the items, or requiring the items to share the height of the tallest item. Here's a visual aid:

How can we leverage self-sizing, but then additional require that all of the items in the group must share the height of the tallest item?

In a more general sense, you can think of this as an alignment issue. How do you leverage self-sizing but then control the alignment of the items within the group? By default, they're top-aligned. That's often fine, but sometimes you might want center-aligned or even bottom-aligned. And the case I'm asking about could be thought of as "fully justified" (if we interpret that to mean, "stretching all of the items to fill the available height, which itself is dictated by the height of the tallest item."

I've resolved this situation for grids by subclassing UICollectionViewCompositionalLayout and modifying attributes received from super in layoutAttributesForElements(in:). Here I take attributes with same frame.origin.y and apply frame.size.height of the talles among them. Be sure you take attributes where attributes.representedElementCategory == .cell. Some extra logic may be required, but it works for grid and introduces issues for orthogonal sections. So I've also added per-section check if I need to modify items in this specific indexPath.section.

@elmodos can you share some sample code for how you managed this? I get an exception with layoutAttributesForItem called thousands of time if the frame height is altered al all...

How to: Compositional layout with self-sizing rows of N columns where the height of each item is set to the tallest item in its row
 
 
Q