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 UICollectionViewCells 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?