The new collection view compositional layout available in iOS 13 and up represents significant shift from the old way of creating complex collection views layouts. It is a declarative approach to building complex layouts with UICollectionView.
Compositional layouts are a declarative kind of API that allows us to build large layouts by stitching together smaller layout groups. Compositional layouts have a hierarchy that consists of Item, Group, Sections, and Layout.
To build any compositional layout, the following four classes need to be implemented:
UICollectionViewCompositionalLayout
which can be set on UICollectionView
with the setCollectionViewLayout
method. UICollectionViewCompositionalLayout
has two main initializers. One takes parameter section:
which is of type NSCollectionLayoutSection
and the other takes sectionProvider:
which is basically a closure that lets us provide different sections based on the indexPath
.NSCollectionLayoutSection
either directly or via the section provider. This represents the standard section we know from table views and collection views. This class has a number of important properties to customize how the section looks like and behaves. Sections eventually compose the compositional layouts. A single section can contain multiple groups.NSCollectionLayoutGroup
available on the class itself and these are .horizontal
, .vertical
and .custom
. Groups are considered to be the biggest workhorse of a compositional layout. You will typically do most configuration work on groups.NSCollectionLayoutItem
is the basic building block of compositional layout. NSCollectionLayoutItems
map directly to the data source items. The item expects instance of NSCollectionLayoutSize
, which is also used to configure sizes of the groups. There are three ways to configure sizes. We have .estimated
, .absolute
and .fractional(Width|Height)
. NSCollectionLayoutDimension
which can be defined by setting the fraction width/height of the layout (percentage relative to its container), or by setting the absolute or estimated sizes.In this post, I have used the UICollectionViewCompositionalLayout
with UICollectionViewDiffableDataSource.
First, create one method returning UICollectionViewCompositionalLayout
. You have to assign that method to collectionViewLayout
in the initialization method.
func createLayout() -> UICollectionViewLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalWidth(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.5)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) let layout = UICollectionViewCompositionalLayout(section: section) return layout }
Here we have used NSCollectionLayoutGroup.horizontal
to create the layout group. By making the layout group horizontal all items added to the group will be positioned horizontally. This means that the group will position items on the horizontal axis until it has filled up its width, and then it starts positioning items on the next "line" until that contains as many items as can be fit in the group’s width and so forth.
Here we have used fraction width and height for NSCollectionLayoutSize
, which means it will fractionally set width and height according to device width and height. We can also use absolute and estimated height and width as well. Absolute will provide an exact height and width. By using estimated will be determined when the content is rendered. There is one restriction for estimated is that you must not set contentInsets for element otherwise, Layout will warn you and won’t draw respectively.
There are two possible ways to give the space to your collection view cells.
By using edge insets you can apply like below.
item.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
You can also provide spacing using the following code.
group.interItemSpacing = .fixed(15)
Example. List view
import UIKit import SnapKit struct Movie: Hashable { let id = UUID() let title: String func hash(into hasher: inout Hasher) { hasher.combine(id) } static func ==(lhs: Movie, rhs: Movie) -> Bool { return lhs.id == rhs.id } static func items() -> [Movie] { var movies: [Movie] = [] for i in 1...7 { movies.append(Movie(title: "Movie \(i)")) } return movies } } class MovieCell: UICollectionViewCell { static let reuseIdentifier = "cellId" let titleLabel: UILabel = { let v = UILabel() v.font = UIFont.systemFont(ofSize: 14) v.textColor = .darkGray return v }() override init(frame: CGRect) { super.init(frame: frame) setupView() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func setupView(){ backgroundColor = .white addSubview(titleLabel) titleLabel.snp.makeConstraints { (make) in make.edges.equalToSuperview() } } } class ViewController: UIViewController, UICollectionViewDelegate { var items: [Movie] = [] enum Section: CaseIterable { case main } lazy var dataSource: UICollectionViewDiffableDataSource<Section, Movie> = { let dataSource = UICollectionViewDiffableDataSource <Section, Movie>(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, movie: Movie) -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MovieCell.reuseIdentifier, for: indexPath) as! MovieCell cell.titleLabel.text = movie.title return cell } return dataSource }() lazy var collectionView: UICollectionView = { let v = UICollectionView(frame: .zero, collectionViewLayout: createLayout()) v.backgroundColor = .white v.register(MovieCell.self, forCellWithReuseIdentifier: MovieCell.reuseIdentifier) return v }() override func viewDidLoad() { super.viewDidLoad() setupView() loadData() setupTimer() } func setupView() { view.addSubview(collectionView) collectionView.snp.makeConstraints { (make) in make.edges.equalToSuperview() } } func loadData() { if items.isEmpty { items = Movie.items() } else { let number = Int.random(in: 0...100) let indx = Int.random(in: 0..<items.count) items.insert(Movie(title: "Movie \(number)"), at: indx) } var snapshot = NSDiffableDataSourceSnapshot<Section, Movie>() snapshot.appendSections([.main]) snapshot.appendItems(items, toSection: .main) dataSource.apply(snapshot, animatingDifferences: true) } func setupTimer() { Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { timer in self.loadData() } } private func createLayout() -> UICollectionViewLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) let layout = UICollectionViewCompositionalLayout(section: section) return layout } }
Example. Grid View
private func createLayout() -> UICollectionViewLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.2)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) let layout = UICollectionViewCompositionalLayout(section: section) return layout }
Example. Two Column View
private func createLayout() -> UICollectionViewLayout { let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0)) let item = NSCollectionLayoutItem(layoutSize: itemSize) let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(44)) let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2) let spacing = CGFloat(10) group.interItemSpacing = .fixed(spacing) let section = NSCollectionLayoutSection(group: group) section.interGroupSpacing = spacing section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10) let layout = UICollectionViewCompositionalLayout(section: section) return layout }
Example. Two rows
Two rows. First one with one item, second one with 3 items.
private func createLayout() -> UICollectionViewLayout { let inset: CGFloat = 5 // Large item on top let topItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.4)) let topItem = NSCollectionLayoutItem(layoutSize: topItemSize) topItem.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset) // Bottom item let bottomItemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .fractionalHeight(1.0)) let bottomItem = NSCollectionLayoutItem(layoutSize: bottomItemSize) bottomItem.contentInsets = NSDirectionalEdgeInsets(top: inset, leading: inset, bottom: inset, trailing: inset) // Group for bottom item, it repeats the bottom item twice let bottomGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.3)) let bottomGroup = NSCollectionLayoutGroup.horizontal(layoutSize: bottomGroupSize, subitem: bottomItem, count: 3) // Combine the top item and bottom group let fullGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalWidth(0.4 + 0.3)) let nestedGroup = NSCollectionLayoutGroup.vertical(layoutSize: fullGroupSize, subitems: [topItem, bottomGroup]) let section = NSCollectionLayoutSection(group: nestedGroup) let layout = UICollectionViewCompositionalLayout(section: section) return layout }