UICollectionView CompositionalLayout Part 2: A List Layout
Get started with your first UICollectionViewCompositionalLayout by making a list layout
To ease into the process of making complex layouts with UICollectionViewCompositionalLayout, we will start by creating a table-view-like list layout.
Our first layout will be a simple list layout, just like how a table view looks. We will use this layout to better get our heads around how UICollectionViewCompositionalLayouts work and then move onto creating more complex layouts.
We create our collection view in the setUpCollectionView function on lines 31 through 43. We initialize the collection view on line 31 by passing in a value for the frame and collectionViewLayout parameters. The value for frame is .zero because will be using autolayout. The value for collectionViewLayout is given to us by the layoutSection function, which we will return to in detail in just a second. The rest of setUpCollectionView does the standard set up for a collection view - sets its dataSource and allows us to use our custom Cell class - and gives our collection view a size.
func setUpCollectionView() {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layoutSection())
collectionView.backgroundColor = .white
collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
collectionView.dataSource = self
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
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
}
Now let’s take a look at where our layout is specified. When can see that the layoutSection function returns a UICollectionViewCompositionalLayout type. We create this type using its initializer with a sectionProvider parameter. This is a closure that gets called any time a layout is needed for a section. Using the sectionIndex value provided, we can then specify a different layout for each section in our collection view, if desired.
func layoutSection() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
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)
return section
})
}
To first make our list layout we need to say how each item in the layout will look, remembering that each cell is an item. All this requires is creating a NSCollectionLayoutItem instance, which in turn requires a NSCollectionLayoutSize instance to say how big each cell will be. On line 17 we create our size instance and pass in .fractionWidth(1.0) and .fractionalHeight(1.0) for the widthDimension and height Dimension parameters respectively. This means that the width of each cell will be 100% of its containing group. The same is true of its height. On line 19, to make each cell separate, we give them a content inset of 2.0 on all sides.
func layoutSection() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
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)
...
})
}
With our item template setup, we now need to create a group template. We use the horizontal initializer of the NSCollectionLayoutGroup class. This will layout each item on a horizontal line until there is no more space on that line, at which point it will move down and continue this process on a new line. Because our cells will take up 100% of the group’s width, each line will only have 1 cell. For the subitems parameter of the initializer, we pass in an array of the items we wish to use in this group, which will just be an array of one thing for us here. We also need to pass in width and height information. Again we use .fractionWidth(1.0) to say the group will take up its container’s entire width. This time for the height though we use .absolute(100.0) which gives the group a height of 50 points. Because each cell takes up 100% of its group’s height, each cell will also then be 100.0 points tall.
func layoutSection() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
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])
...
})
}
The final two steps to complete this function are to make an NSCollectionLayoutSection from our group and to return this section.
func layoutSection() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
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)
return section
})
}
And that’s it! Running the playground, we can now see our list in action with the layout we wanted. Each cell shows its section index and item number.
In the next tip in this series we will learn how to make section layouts that are a step up in terms of difficulty and we will learn how to make each section scroll horizontally independent of the other section while still allowing the whole collection view to scroll vertically!
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
let view = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 667))
self.view = view
setUpCollectionView()
}
func layoutSection() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
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)
return section
})
}
func setUpCollectionView() {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layoutSection())
collectionView.backgroundColor = .white
collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
collectionView.dataSource = self
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
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 collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 100
}
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 = .red
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.