I want to build an iOS app with a UICollectionViewController
that always have the same number of cells per row. Because I don't want my UICollectionViewController
to deal with too much things, I've refactored my code and implemented funny things like protocol associatedtype
and generic types. Now, my app is composed of 4 different .swift
files.
1. CustomFlowLayout.swift
CustomFlowLayout
is a simple subclass of UICollectionViewFlowLayout
that allows us to set its minimumInteritemSpacing
, minimumLineSpacing
and sectionInset
properties with dependency injection thanks to a convenience initializer.
import UIKit
class CustomFlowLayout: UICollectionViewFlowLayout {
convenience init(minimumInteritemSpacing: CGFloat = 0,
minimumLineSpacing: CGFloat = 0,
sectionInset: UIEdgeInsets = .zero) {
self.init()
self.minimumInteritemSpacing = minimumInteritemSpacing
self.minimumLineSpacing = minimumLineSpacing
self.sectionInset = sectionInset
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
2. ColumnDataSource.swift
ColumnDataSource
is a subclass of NSObject
that conforms to UICollectionViewDataSource
, UICollectionViewDelegate
and UICollectionViewDelegateFlowLayout
. Its implements collectionView(_:layout:sizeForItemAt:)
in order to display the correct number of UICollectionViewCell
s per row. Also note that ColumnDataSource
is a generic class that requires us to pass it a type parameter at initialization.
import UIKit
class ColumnDataSource<FlowLayoutType: UICollectionViewFlowLayout>: NSObject, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
let cellsPerRow: Int
init(cellsPerRow: Int) {
self.cellsPerRow = cellsPerRow
super.init()
}
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let flowLayout = collectionView.collectionViewLayout as! FlowLayoutType
let marginsAndInsets = flowLayout.sectionInset.left + flowLayout.sectionInset.right + flowLayout.minimumInteritemSpacing * (CGFloat(cellsPerRow) - 1)
let itemWidth = (collectionView.bounds.size.width - marginsAndInsets) / CGFloat(cellsPerRow)
return CGSize(width: itemWidth, height: itemWidth)
}
}
3. ColumnFlowLayoutable.swift
The purpose of ColumnFlowLayoutable
protocol is to make sure that any class that conforms to it has columnDataSource
and customFlowLayout
properties where columnDataSource
type's type parameter matches customFlowLayout
type.
import UIKit
protocol ColumnFlowLayoutable {
associatedtype FlowLayoutType: UICollectionViewFlowLayout
var columnDataSource: ColumnDataSource<FlowLayoutType> { get }
var customFlowLayout: FlowLayoutType { get }
}
4. CollectionViewController.swift
CollectionViewController
is a subclass of UICollectionViewController
that conforms to ColumnFlowLayoutable
protocol. It also implement viewWillTransition(to:with:)
in order to deal with container size changes.
import UIKit
class CollectionViewController: UICollectionViewController, ColumnFlowLayoutable {
let columnDataSource = ColumnDataSource<CustomFlowLayout>(cellsPerRow: 2)
let customFlowLayout = {
CustomFlowLayout(minimumInteritemSpacing: $0, minimumLineSpacing: $0, sectionInset: UIEdgeInsets(top: $0, left: $0, bottom: $0, right: $0))
}(10)
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.collectionViewLayout = customFlowLayout
collectionView?.dataSource = columnDataSource
collectionView?.delegate = columnDataSource
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
collectionView?.collectionViewLayout.invalidateLayout()
}
}
The complete project can be found at this Github repo: CollectionViewColumnsProtocol.
This code works fine. I can use it with subclasses of CustomFlowLayout
and it still works. However, I cannot use it with subclasses of ColumnDataSource
.
If I try to build the project by using a subclass of ColumnDataSource
(e.g class SubColumnDataSource: ColumnDataSource<CustomFlowLayout>
) in CollectionViewController
, Xcode throws the following build time error message:
Type 'CollectionViewController' does not conform to protocol 'ColumnFlowLayoutable'
What would I have to change in ColumnFlowLayoutable
protocol in order to allow CollectionViewController
to work with subclasses of ColumnDataSource
?