'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: 
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:
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):
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 |


