iosswiftuicollectionviewuikituicollectionviewcompositionallayout

Why does my single-section, 2-group compositional layout only use the sizing of the first group in the section?


I am trying to create a compositional layout comprised of a group of 2 items, plus another group of 1 item. I would like each group to occupy the full screen width, and page-scroll between them. So the 2 items in the first group (Items A) would each occupy 50% of the screen width, and the 1 item (Item B) in the second group would occupy 100% of the screen width. I have the collection view pinned to the left and right edges of its superview, which occupies the entire screen.

A textual diagram would appear something like this:

[Item A][Item A]|[    Item B    ]

Where "|" denotes the edge of the screen. So depending on scroll position, either both Items A will be visible, or Item B will be visible. Item A and Item B should never be visible at the same time after scrolling has ended.

I am using the following code for my compositional layout:

UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))

    let planItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))
    let planItem = NSCollectionLayoutItem(layoutSize: planItemSize)
    let planGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: planItem, count: 2)

    let formItem = NSCollectionLayoutItem(layoutSize: groupSize)
    let formGroup = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: formItem, count: 1)

    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [planGroup, formGroup])
    let section = NSCollectionLayoutSection(group: group)
    section.orthogonalScrollingBehavior = .groupPaging
    return section
}

The above code is resulting in a layout that looks like this:

[Item A][Item A]|[Item B]

It is nearly correct except that Item B is being sized at 50% width of the screen instead of 100%. I can't figure out what's wrong with the layout, but it appears to be using planItemSize for all items, instead of just for Items A. How do I get Item B to be sized at 100% the screen width?


Solution

  • First, we could do this very easily with a Horizontal Flow Layout, .isPagingEnabled = true, and implementing sizeForItemAt:

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    
        let w: CGFloat = collectionView.frame.width
        let h: CGFloat = collectionView.frame.height
    
        if indexPath.item % 3 == 2 {
            return .init(width: w, height: h)
        }
        
        return .init(width: w * 0.5, height: h)
        
    }
    

    and we're done :)

    However, since you're asking about UICollectionViewCompositionalLayout, let's look at that.

    When working with items, groups and sub-groups, .fractionalWidth() and .fractionalHeight() is relative to the parent.

    Since we're using 100% for all heights in this layout, we'll focus on widths.

    For a "basic" layout, .fractionalWidth(1.0) will be 100% of the width of the collection view.

    Using .fractionalWidth(0.5) will, of course, be 50% of the width of the collection view.

    When we put an items in a group, the count: affects the size and the item width is (apparently) ignored.

    So, if we set a group to .fractionalWidth(1.0) (100% of the width of the collection view), and set count to 4:

    let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1))
    let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1))
    let item = NSCollectionLayoutItem(layoutSize: itemSize)
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 4)
    let section = NSCollectionLayoutSection(group: group)
    return section
    

    We'll get this (red outline is collection view frame):

    enter image description here

    We can change itemSize .fractionalWidth(1.0) to 0.1, 0.5, 2.0, etc... it doesn't change the layout.


    That said, in your current code, you are setting both planGroup and formGroup to 100% width of their parent - group - which is also 100% of the width of the collection view.

    So, only the first group fits, and the second group is never used.

    Let's fix that (I'm going to change group to mainGroup to make it easier to talk about).

    What we can do instead is set planGroup to .fractionalWidth(0.5) and formGroup to .fractionalWidth(0.5) ... and we'll get close:

    enter image description here

    But - how do we get two items to fill the collection view, followed by one item, followed by two items, etc?

    We can set the mainGroup width to 200% of the collection view width:

    enter image description here

    Now we get the desired "pattern." Note that we will want to use .paging instead of .groupPaging because we want to page by collection view Width:

    enter image description here

    enter image description here

    Here is a modified version of your code - you should be able to drop it right in as a replacement:

        UICollectionViewCompositionalLayout {
            (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            
            // main group size is TWICE as wide as the collection view
            let mainGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(2.0), heightDimension: .fractionalHeight(1))
            
            // sub-groups
            //  width is Percentage-of-Parent
            //  so if each sub-group is one-half the width of the main group
            //  each will be the Full width of the collection view
            let planGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))
            let formGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1))
            
            // because we're putting TWO planItems in planGroup
            //  CompositionLayout will use 50% of planGroup width for each item
            //  so planItemSize width is ignored
            let planItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1))
            let planItem = NSCollectionLayoutItem(layoutSize: planItemSize)
            let planGroup = NSCollectionLayoutGroup.horizontal(layoutSize: planGroupSize, subitem: planItem, count: 2)
            
            // because we're putting ONE formItem in formGroup
            //  CompositionLayout will use 100% of formGroup width
            //  so formItemSize width is ignored
            let formItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1))
            let formItem = NSCollectionLayoutItem(layoutSize: formItemSize)
            let formGroup = NSCollectionLayoutGroup.horizontal(layoutSize: formGroupSize, subitem: formItem, count: 1)
            
            let mainGroup = NSCollectionLayoutGroup.horizontal(layoutSize: mainGroupSize, subitems: [planGroup, formGroup])
            let section = NSCollectionLayoutSection(group: mainGroup)
            
            // use .paging instead of .groupPaging
            //  because we want to page by collection view Width
            section.orthogonalScrollingBehavior = .paging
            
            return section
            
        }