UICollectionView CompositionalLayout Part 3: Side-By-Side Cells
Continue your journey with UICollectionViewCompositionalLayout
Learn how to make rows with multiple cells.
In this tip we will add a different layout for the second section of our collection view. This layout consists of two equally sized side-by-side cells. We will also learn how to implement orthogonal scrolling to allow the cells within a section to be scrolled in the opposite direction of the collection view as a whole.
Our setup this time around is mostly the same. We are now using a Section enum to help us keep our code for the different section layouts clean and organized. The rest of the setup for the collection view is the same. You can see we use the sectionIndex variable provided to us by the sectionProvider closure to create an instance of the Section enum. We then return the sectionLayout property from this enum, it is an NSCollectionLayoutSection like we are asked to return.
func setUpCollectionView() {
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
let section = Section(rawValue: sectionIndex)!
return section.sectionLayout
})
...
}
We can see on lines 12 and 13 that the value for the sectionLayout property is computed based on the case of the enum it is called on. Each case calls a different function to get its corresponding layout.
var sectionLayout: NSCollectionLayoutSection {
switch self {
case .first: return self.section1()
case .second: return self.section2()
}
}
The section1 function has the code for the first section, see the previous Tip in this series for more details on the setup for this layout. The section2 function has the code for the new layout we are creating in this Tip.
private func section1() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100.0)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
private func section2() -> NSCollectionLayoutSection {
...
}
Line 40 holds the only difference between the code in the section2 function and the section1 function. In the second section we want each cell to take up half of the width of its group, so we set the widthDimension parameter of its layout size to be .fractionalWidth(0.5) instead of .fractionalWidth(1.0). If we wanted 3 cells per group, we would use 0.333 instead of 0.5. Changing just one constant gives us significantly different layouts!
private func section1() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
...
}
private func section2() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
...
}
The ability to have the collection view scroll vertically while having each section scroll horizontally is achieved by using the orthogonalScrollingBehavior property on each section. On lines 33 and 47 we set this property to have a value of .paging. To see what other options we have for this scrolling behavior, take a look at the the corresponding Tip in this series.
private func section1() -> NSCollectionLayoutSection {
...
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
private func section2() -> NSCollectionLayoutSection {
...
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
We have now taken another step in our ability to create layouts with UICollectionViewCompositionalLayout. To learn how to complete the final 2 layouts from our original demo, continue onto the next Tip in this series.
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
enum Section: Int, CaseIterable {
case first, second
var sectionLayout: NSCollectionLayoutSection {
switch self {
case .first: return self.section1()
case .second: return self.section2()
}
}
var color: UIColor {
switch self {
case .first: return .red
case .second: return .purple
}
}
private func section1() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100.0)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
private func section2() -> NSCollectionLayoutSection {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 2.0, leading: 2.0, bottom: 2.0, trailing: 2.0)
let group = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(100.0)), subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = .paging
return section
}
}
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 667))
self.view = view
setUpCollectionView()
}
func setUpCollectionView() {
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
let section = Section(rawValue: sectionIndex)!
return section.sectionLayout
})
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.backgroundColor = .white
collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
collectionView.dataSource = self
view.addSubview(collectionView)
collectionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
}
}
extension MyViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return Section.allCases.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.resuseIdentifier, for: indexPath) as! Cell
cell.label.text = "(\(indexPath.section), \(indexPath.row))"
cell.backgroundColor = Section(rawValue: indexPath.section)!.color
return cell
}
}
class Cell: UICollectionViewCell {
static let resuseIdentifier = "Cell"
let label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(label)
label.topAnchor.constraint(equalTo: topAnchor).isActive = true
label.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
label.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
PlaygroundPage.current.liveView = MyViewController()
* This article originally appeared in the ZipTips app.
Want to connect?
Contact me to talk about this article, working together or something else.