Useful extensions for CollectionView and Compositional Layout
With a few extensions, you can make working with CollectionViews and Compositional Layout a lot cleaner.
Published: Feb. 6, 2021 Sponsored App StoreSwift's extensions are powerful tool for reducing boilerplate code and simplifying working with APIs. Among other things. In this post, I would like to share the extensions that I am using for better working with CollectionViews and also with Compositional Layout.
I am not trying to say that these are the best you can find, they are intended more as an inspiration. On the hand, feel free to use these in your projects.
CollectionViews
Working with CollectionViews involves a lot of reuse identifiers. These are strings used to specify what cells or supplementary views we want to register and later dequeue.
One of the basic extensions I use regularly is this one:
extension UICollectionReusableView {
static var reuseIdentifier: String {
return String(describing: Self.self)
}
}
With this extension every cell and supplementary view has reuseIdentifier
property so we don't have to type it by hand or create constants somewhere else. Also my other extensions build upon this one.
Registration
When registering a cell for use with CollectionView, we need to provide the class and the reuse identifier separately. Since all our cells have standardized reuse identifier, we can create simplified version like this:
extension UICollectionView {
func register<T: UICollectionViewCell>(cell: T.Type) {
register(T.self, forCellWithReuseIdentifier: T.reuseIdentifier)
}
}
And the usage then looks like this:
collectionView.register(cell: ColorCell.self)
This pattern works even better for header views and other supplementary views. Because traditionally this is how you'd register header class:
collectionView.register(SimpleHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: SimpleHeaderView.reuseIdentifier)
We can create this extension:
extension UICollectionView {
func register<T: UICollectionReusableView>(header: T.Type) {
register(T.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: T.reuseIdentifier)
}
}
And now the usage is greatly simplified:
collectionView.register(header: SimpleHeaderView.self)
It is not about the amount of code. With this you don't have to think about the header constant defined on UICollectionView
and separately specify the reuse identifier.
If you are frequently working with footers, you can define basically the same extension:
func register<T: UICollectionReusableView>(footer: T.Type) {
register(T.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: T.reuseIdentifier)
}
If you are using .xib
files to design your cells and supplementary views then it makes sense to create similar helper methods for those variants.
Dequeue
Dequeueing of cells and supplementary views is another common task that involves passing the indexPath, reuse identifier and also casting the cell to the correct type. We can make this much better with extensions.
extension UICollectionView {
func dequeue<T: UICollectionViewCell>(for indexPath: IndexPath) -> T {
return dequeueReusableCell(withReuseIdentifier: T.reuseIdentifier, for: indexPath) as! T
}
}
I am using as!
because if the cell is not a correct type then it is a programmer error. I don't think it makes any sense trying to avoid it and maybe just return the cell without casting it somehow.
Anyway, with this extension, we can transform this common code:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ColorCell.reuseIdentifier, for: indexPath) as! ColorCell
Into this:
let cell: ColorCell = collectionView.dequeue(for: indexPath)
Now we can just specify what kind of cell we want and pass the indexPath. Nothing else is needed.
Compositional Layout
Let's look at extensions specifically for working with Compositional Layout. When setting content insets on items, groups or sections, we are using the NSDirectionalEdgeInsets
type which has just a single init
where you need to specify all the edges. So I am frequently using these two extensions:
extension NSDirectionalEdgeInsets {
static func uniform(size: CGFloat) -> NSDirectionalEdgeInsets {
return NSDirectionalEdgeInsets(top: size, leading: size, bottom: size, trailing: size)
}
init(horizontal: CGFloat, vertical: CGFloat) {
self.init(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal)
}
}
This makes it easy to have same insets on all sides with uniform
and eliminates the possibility you configure one edge incorrectly.
The usage then looks like this:
item.contentInsets = .uniform(size: 5)
And this:
item.contentInsets = .init(horizontal: 5, vertical: 0)
Another approach might be to define specific sizes in the extension like this:
static func small() -> NSDirectionalEdgeInsets {
return .uniform(size: 5)
}
static func medium() -> NSDirectionalEdgeInsets {
return .uniform(size: 15)
}
Now you have standardized system and don't have to worry about using 15 on one screen and then by mistake using 16.
This also possibly allows you to tweak sizes based on device size and maybe increase them for iPads. With extensions like these there are just a few places where you need to make the edits.
Extending layout items
Declaring Compositional Layout can quickly get a repetitive. So you can experiment by extending the NSCollectionLayoutItem
. For example:
extension NSCollectionLayoutItem {
static func withEntireSize() -> NSCollectionLayoutItem {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
return NSCollectionLayoutItem(layoutSize: itemSize)
}
}
Entire fractional size is something I am using a lot. I am not super happy with the naming there though.
I am also frequently using variant of this where the height is either estimated or can be passed as a parameter. Something like this:
static func entireWidth(withHeight height: NSCollectionLayoutDimension) -> NSCollectionLayoutItem {
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: height)
return NSCollectionLayoutItem(layoutSize: itemSize)
}
You can also go a bit further and extend entire section, like this:
extension NSCollectionLayoutSection {
static func listSection(withEstimatedHeight estimatedHeight: CGFloat = 100) -> NSCollectionLayoutSection {
let layoutItem = NSCollectionLayoutItem.withEntireSize()
layoutItem.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 15, bottom: 10, trailing: 15)
let layoutGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(estimatedHeight))
let layoutGroup = NSCollectionLayoutGroup.vertical(layoutSize: layoutGroupSize, subitems: [layoutItem])
layoutGroup.interItemSpacing = .fixed(10)
return NSCollectionLayoutSection(group: layoutGroup)
}
}
Now it is much easier to build list screens with Compositional Layout. And since we have listSection
defined, we can define listLayout
:
extension UICollectionViewCompositionalLayout {
static func listLayout() -> UICollectionViewCompositionalLayout {
return UICollectionViewCompositionalLayout(section: .listSection())
}
}
And now when we want simple list-based screen, we can do something like this:
func createLayout() -> UICollectionViewLayout {
return UICollectionViewCompositionalLayout.listLayout()
}
Or inline this:
collectionView.setCollectionViewLayout(UICollectionViewCompositionalLayout.listLayout(), animated: false)
There's obviously much more room for additional extensions. It all depends on what makes sense for your project. But I think at least a few extensions can go a long way towards better experience with Compositional Layout. If you app has a few list screens it does not make sense to define the list layout for each screen separately (assuming they are wildly different).
I am trying to avoid these extensions in my tutorials, because then someone can copy the code and wonder why certain methods don't exist.
Thanks for reading!
Uses: Xcode 12 & Swift 5.3