'How to center cells inside collection view using collection view compositional layout

I have the following task at work: I need to implement the cinema view where I have a screen and a collection of seats where each row represents a section: enter image description here

Data is coming from a 2-dimensional array that represents the seats:

var seats = [[1, 2, 3, 4, 5],
             [1, 2, 3, 4, 5],
            [1, 2, 3, 4, 5, 6],
            [1, 2, 3, 4, 5, 6],
            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
            [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
]

The issues is that I can't seem to find how to center the cells inside collection view using compositional layout. Here's the code that manages the layout:

private func createLayout() -> UICollectionViewLayout {
        let layout = UICollectionViewCompositionalLayout { [weak self] (section, configuration) -> NSCollectionLayoutSection? in
            guard let self = self else { return nil }
            
            // get the max seat number
            let max = CGFloat(self.seats.compactMap { $0.max() }.max()!)

            let itemSize = NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0 / max),
                heightDimension: .fractionalHeight(0.5))
            let item = NSCollectionLayoutItem(layoutSize: itemSize)
            item.contentInsets = .init(top: 2, leading: 2, bottom: 2, trailing: 2)

            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                                                         heightDimension: .fractionalWidth(0.2))

            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])

            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = .init(top: 16, leading: 32, bottom: 0, trailing: 32)
            
            return section
        }
        
        let config = UICollectionViewCompositionalLayoutConfiguration()
        config.interSectionSpacing = -50
        layout.configuration = config
        
        return layout
    }

I managed to implement this behavior using an "old-school" collection view flow layout like this:

enter image description here

Obviously I can't use methods of Collection View Flow Layout like collectionView:layout:insetForSectionAtIndex: to calculate this, so any ideas would be much appreciated!



Solution 1:[1]

Let each row, or at least each set of rows with the same number of items, be its own section, and give the layout a section provider function. The section provider function is given the section number along with an environment object. From that environment object, you can learn the width of the collection view. Now just give the group or section appropriate insets.

To illustrate, I was able to achieve a layout like this (three sections, number of items per section is the section index plus one):

enter image description here

And I won't keep you in suspense; I'll just show you the code for that layout. It isn't pretty; I used a bunch of hard-coded values. The idea was just to generate the layout so as to show you the general principles. But I'm sure you can see how to adapt this to your own situation:

let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .vertical
let inter = 5 as CGFloat
config.interSectionSpacing = 5
let layout = UICollectionViewCompositionalLayout(sectionProvider: { ix, environment in
    let side = 30 as CGFloat
    let cellsz = NSCollectionLayoutSize(widthDimension: .absolute(side), heightDimension: .fractionalHeight(1))
    let cell = NSCollectionLayoutItem(layoutSize: cellsz)
    let groupsz = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(side))
    let count = ix+1
    let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupsz, subitems: [cell])
    group.interItemSpacing = .fixed(inter)
    let sec = NSCollectionLayoutSection(group: group)
    let width = environment.container.contentSize.width
    let inset = (width - CGFloat(count)*side - (CGFloat(count)-1)*inter) / 2.0
    sec.contentInsets = .init(top: 0, leading: inset, bottom: 0, trailing: 0)
    return sec
}, configuration: config)

That's probably not the only way; you could instead, I think, call NSCollectionLayoutGroup.custom and just lay out the cells manually. But I think this is not a bad solution.

I should also say that I'm not persuaded that a collection view is the best way to get the result you're trying to achieve. You might do better just to lay out the seat views yourself, directly, in code.

Solution 2:[2]

I have the same problem while implementing my own keyboard. My result: Keyboard images. This is how I did it without using constants (an improvement of Matt's awesome answer).

  private func generateKeyboardLayout() -> UICollectionViewLayout {

    let groupInset = 3 / UIScreen.main.scale
    let layout = UICollectionViewCompositionalLayout { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
      let item = NSCollectionLayoutItem(
        layoutSize: NSCollectionLayoutSize(
          widthDimension: .fractionalWidth(1.0),
          heightDimension: .fractionalHeight(1.0)))
      
      let group = NSCollectionLayoutGroup.horizontal(
        layoutSize: NSCollectionLayoutSize(
          widthDimension: .fractionalWidth(1/10),
          heightDimension: .fractionalHeight(1/3)),
        subitems: [item])
      group.contentInsets = NSDirectionalEdgeInsets(top: groupInset, leading: groupInset, bottom: groupInset, trailing: groupInset)
      
      let count = Keyboard.getItems(at: keyboardSection).count
      let containerWidth = layoutEnvironment.container.contentSize.width
      let groupWidthDimension = group.layoutSize.widthDimension.dimension
      let itemWidth = containerWidth * groupWidthDimension
      let inset = (containerWidth - CGFloat(count) * itemWidth) / 2.0
  
      let section = NSCollectionLayoutSection(group: group)
      section.contentInsets = .init(top: 0, leading: round(inset), bottom: 0, trailing: 0)
      section.orthogonalScrollingBehavior = .continuous
      return section
    }
    return layout
  }

count represents the number of items per row

let count = Keyboard.getItems(at: keyboardSection).count

containerWidth is the exact width of the view in CGFloat

let containerWidth = layoutEnvironment.container.contentSize.width

groupWidthDimension is the width of each group / keyboard tile including the applied insets

let groupWidthDimension = group.layoutSize.widthDimension.dimension

itemWidth gives you the exact size of each keyboard tile

let itemWidth = containerWidth * groupWidthDimension

inset (leading) is calculated based on the difference of the containerWidth and the sum of all the items' width and then halved

let inset = (containerWidth - CGFloat(count) * itemWidth) / 2.0
section.contentInsets = .init(top: 0, leading: round(inset), bottom: 0, trailing: 0)

Solution 3:[3]

You could use NSCollectionLayoutGroup.custom(layoutSize:) where we can set the frame using NSCollectionLayoutGroupCustomItemProvider.

Set the frame for each item using

var customItems = [NSCollectionLayoutGroupCustomItem]()
            for item in 0..<itemCount {
                let frame = CGRect(x: yourCalculatedMargin, y: yPosition, width: layoutSize.widthDimension.dimension, height: layoutSize.heightDimension.dimension)
                customItems.append(NSCollectionLayoutGroupCustomItem(frame: frame))
            }
            return customItems

Remember to calculate the yPosition too for moving on to next line

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Gil
Solution 3 Lakshmi C