nknr
nknr

Reputation: 133

UICollectionViewCompositionalLayout - center items in sections or groups

I set up a collection view and its layout with the new compositional stuff and it has been pretty cool, it seems to be very expandable but I can't find a way to make items centered in the collection, which is IMO a basic feature that one would imagine being supported..

What I have is:

enter image description here

Accomplished with this code:

UICollectionViewCompositionalLayout { section, env in
    let tagDefaultSize = CGSize(width: 100, height: 40)
    
    let item = NSCollectionLayoutItem(
        layoutSize: NSCollectionLayoutSize(widthDimension: .estimated(tagDefaultSize.width), heightDimension: .absolute(tagDefaultSize.height))
    )
    
    item.edgeSpacing = NSCollectionLayoutEdgeSpacing(
        leading: .fixed(0),
        top: .fixed(0),
        trailing: .fixed(8),
        bottom: .fixed(0)
    )
    
    let group = NSCollectionLayoutGroup.horizontal(
        layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(tagDefaultSize.height)),
        subitems: [item]
    )
    
    group.edgeSpacing = NSCollectionLayoutEdgeSpacing(
        leading: .flexible(8),
        top: .fixed(0),
        trailing: .fixed(8),
        bottom: .fixed(8)
    )
    
    let section = NSCollectionLayoutSection(group: group)
    
    section.contentInsets = NSDirectionalEdgeInsets(
        top: 30,
        leading: 20,
        bottom: 40,
        trailing: 20
    )
    
    return section
}

But what I need is this:

enter image description here

Has anyone had any luck with this? Thanks

Upvotes: 6

Views: 2411

Answers (1)

Ashley Mills
Ashley Mills

Reputation: 53181

I found a solution for this, using

custom(layoutSize: NSCollectionLayoutSize, itemProvider: @escaping NSCollectionLayoutGroupCustomItemProvider)

I created the following extension, which calculates the frames of each element in the group, centring as many items as will fit on each row

extension NSCollectionLayoutGroup {

    static func verticallyCentered(cellSizes: [CGSize], interItemSpacing: CGFloat = 10, interRowSpacing: CGFloat = 10) -> NSCollectionLayoutGroup {
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .estimated(100))
        return custom(layoutSize: groupSize) { environment in
            var items: [NSCollectionLayoutGroupCustomItem] = []
            
            var yPos: CGFloat = environment.container.contentInsets.top
            
            var rowSizes: [CGSize] = []
            
            func totalWidth() -> CGFloat {
                rowSizes.map(\.width).reduce(0) {
                    $0 == 0 ? $1 : $0 + interItemSpacing + $1
                }
            }
            
            func addRowItems() {
                var xPos = (environment.container.effectiveContentSize.width - totalWidth())/2 + environment.container.contentInsets.leading
                let maxItemHeight = rowSizes.map(\.height).max() ?? 0
                let rowItems: [NSCollectionLayoutGroupCustomItem] = rowSizes.map {
                    let rect = CGRect(origin: CGPoint(x: xPos, y: yPos + (maxItemHeight - $0.height) / 2), size: $0)
                    xPos += ($0.width + interItemSpacing)
                    return NSCollectionLayoutGroupCustomItem(frame: rect)
                }
                
                items.append(contentsOf: rowItems)
            }
            
            for (index, cellSize) in cellSizes.enumerated() {
                rowSizes.append(cellSize)
                
                if totalWidth() > environment.container.effectiveContentSize.width {
                    rowSizes.removeLast()
                    addRowItems()
                    yPos += (cellSize.height + interRowSpacing)
                    rowSizes = [cellSize]
                }
                
                if index == cellSizes.count - 1 {
                    addRowItems()
                }
            }
            return items
        }
    }
}

Then create a layout section with a single group that contains all the items in that section. First you'll first need to calculate the sizes of all the elements in the section, and then pass those in to the function above, something like this (note that I'm using a DiffableDataSource here)…

func someLayoutSection(sectionIndex: Int) -> NSCollectionLayoutSection {
    let itemCount = collectionView.numberOfItems(inSection: sectionIndex)
    let cell = SomeCell(frame: .zero)
    let cellSizes: [CGSize] = (0..<itemCount).compactMap {
        switch diffableDataSource.itemIdentifier(for: IndexPath(item: $0, section: sectionIndex)) {
        case let .someItem(something):
            cell.configure(thing: something)
            return cell.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        default:
            return nil
        }
    }
    
    let group = NSCollectionLayoutGroup.verticallyCentered(cellSizes: cellSizes)
    group.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20).scaled
    
    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0)
    return section
}

and then create the layout…

func makeLayout() -> UICollectionViewLayout {
    UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in

        let section = diffableDataSource.snapshot().sectionIdentifiers[sectionIndex]

        switch section {
        case .someSection:
            return someLayoutSection(sectionIndex: sectionIndex)
        case .otherSection:
            // etc
        }
    }

Upvotes: 9

Related Questions