Bidirectional collection view with orthogonalScrollingBehavior
Before iOS 13, if you wanted to have a vertical collection view with horizontally scrolling sections it required embedding a scroll view inside the collection view cell, passing the model around, handling touch events, a lot of delegation, and complicated logic. With UICollectionViewCompositionalLayout
all of those problems fading away. Apple gave us orthogonalScrollingBehavior
.
We will build our new section using code from my last post.
Let's start with our model. I want to display a horizontally scrolled section displaying just letters. Modify model class by adding new var:
lazy var letters: [Character] = {
let aScalars = "a".unicodeScalars
let aCode = aScalars[aScalars.startIndex].value
return (0..<26).map { i in
Character(Unicode.Scalar(aCode + i) ?? aScalars[aScalars.startIndex])
}
}()
Now simple cell, just label for letter:
final class LetterCell: UICollectionViewCell {
private(set) lazy var letterLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
private func setupViews() {
contentView.addSubview(letterLabel)
letterLabel.translatesAutoresizingMaskIntoConstraints = false
layer.borderWidth = 2
layer.borderColor = UIColor.black.cgColor
layer.cornerRadius = 5
NSLayoutConstraint.activate(
[
contentView.centerXAnchor
.constraint(equalTo: contentView.centerXAnchor),
contentView.centerYAnchor
.constraint(equalTo: contentView.centerYAnchor)
letterLabel.centerXAnchor
.constraint(equalTo: contentView.centerXAnchor),
letterLabel.centerYAnchor
.constraint(equalTo: contentView.centerYAnchor)
]
)
}
}
So far so good. Now, adjust our view controller, it needs to know about the new section and new cell.
In func prepareCollectionView()
register new cell:
collectionView.register(LetterCell.self, forCellWithReuseIdentifier: "letterCell")
Our UICollectionViewDataSource
have to return two sections and handle cell preparation:
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section % 2 == 0 {
return model.letters.count
}
return model.colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.section % 2 == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "letterCell", for: indexPath) as! LetterCell
cell.letterLabel.text = String(model.letters[indexPath.row])
return cell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = model.colors[indexPath.row]
cell.layer.cornerRadius = 5
cell.layer.masksToBounds = true
return cell
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let cell = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
withReuseIdentifier: kind,
for: indexPath
)
if let header = cell as? HeaderView {
header.label.text = kind
}
return cell
}
}
We are almost there. The last thing is to modify our layout:
enum GridCompositionalLayout {
static func generateLayout() -> UICollectionViewCompositionalLayout {
let config = UICollectionViewCompositionalLayoutConfiguration()
config.boundarySupplementaryItems = [makeCollectionHeader()]
// 1
return UICollectionViewCompositionalLayout(
sectionProvider: { section, _ in
if section % 2 == 0 {
return makeLetterSection()
}
return makeSection()
},
configuration: config
)
}
/// old code unchanged
}
// new extension added below
extension GridCompositionalLayout {
private static func makeLetterItem() -> NSCollectionLayoutItem {
let item = NSCollectionLayoutItem(
layoutSize: .init(
widthDimension: .fractionalWidth(1),
heightDimension: .fractionalHeight(0.5)
)
)
item.contentInsets = .init(top: 3, leading: 3, bottom: 3, trailing: 3)
return item
}
private static func makeLetterGroup() -> NSCollectionLayoutGroup {
let group = NSCollectionLayoutGroup.vertical(
layoutSize: .init(
widthDimension: .fractionalWidth(3/8),
heightDimension: .fractionalWidth(6/8)
),
subitems: [makeLetterItem()]
)
return group
}
private static func makeLetterSection() -> NSCollectionLayoutSection {
let section = NSCollectionLayoutSection(group: makeLetterGroup())
section.contentInsets = .init(
top: 16,
leading: 0,
bottom: 16,
trailing: 0
)
// 2
section.orthogonalScrollingBehavior = .groupPagingCentered
return section
}
}
The biggest changes are:
- Now instead of returning one section, we have a factory method returning different sections layouts depending on the section number
- New section layout. There is nothing special about it at the first glance. One item, one simple group. Biggest change is
section.orthogonalScrollingBehavior = .groupPagingCentered
- setting this makes the section scroll perpendicularly to the main axis of our collection view. And this is the only thing needed to make such a section work. No embedding, no delegation, just one simple line of code!
And the effect:
The full code can be found here.