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
}