Based on this article I have created a reusable data source for UICollectionView
as follows :-
final class CollectionViewDataSource<Model>: NSObject, UICollectionViewDataSource {
typealias CellConfigurator = (Model, UICollectionViewCell) -> Void
var models: [Model] = []
private let reuseIdentifier: String
private let cellConfigurator: CellConfigurator
init(reuseIdentifier: String, cellConfigurator: @escaping CellConfigurator) {
self.reuseIdentifier = reuseIdentifier
self.cellConfigurator = cellConfigurator
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return models.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = models[indexPath.item]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cellConfigurator(model, cell)
return cell
}
}
I have then extended this class so I can provide 'cell specific' setup based on the type of model
extension CollectionViewDataSource where Model == HomeFeedItem {
static func make(reuseIdentifier: String = "FEED_ARTICLE_CELL") -> CollectionViewDataSource {
return CollectionViewDataSource(reuseIdentifier: reuseIdentifier, cellConfigurator: { item, cell in
(cell as? FeedArticleCell)?.render(with: item)
})
}
}
extension CollectionViewDataSource where Model == HomeFeedAlertItem {
static func make(reuseIdentifier: String = "FEED_ALERT_CELL") -> CollectionViewDataSource {
return CollectionViewDataSource(reuseIdentifier: reuseIdentifier, cellConfigurator: { item, cell in
(cell as? FeedAlertCell)?.render(with: item)
})
}
}
This is working perfect, however, each of these cells has a different design but does in fact accept very similar properties (as do the other cells) - because of this I was thinking of creating a simple FeedItemModel
and mapping these properties prior to rendering my feed. This would ensure anywhere I rendered a feed item, I was always dealing with the same properties.
With that in mind I tried to create something like :-
extension CollectionViewDataSource where Model == FeedItemModel {
static func make(reuseIdentifier: String = "FEED_ARTICLE_CELL") -> CollectionViewDataSource {
return CollectionViewDataSource(reuseIdentifier: reuseIdentifier, cellConfigurator: { item, cell in
switch item.type {
case .news: (cell as? FeedArticleCell)?.render(with: item)
case .alert: (cell as? FeedAlertCell)?.render(with: item)
}
})
}
}
This however falls down as the reuseIdentifier
field is no longer correct if item.type
is .alert
.
How can I refactor this pattern to allow me to use different cells types with the same model? Or should I abandon this approach and stick to a different model for each cell type regardless of the input properties being the same?
You can create a protocol such as
protocol FeedRenderable {
var reuseIdentifier: String { get }
}
Then ensure the Model
type conforms to FeedRenderable
.
You can then refactor your CollectionViewDataSource
to
final class CollectionViewDataSource<Model>: NSObject, UICollectionViewDataSource where Model: FeedRenderable {
typealias CellConfigurator = (Model, UICollectionViewCell) -> Void
var models: [Model] = []
private let cellConfigurator: CellConfigurator
init(_ cellConfigurator: @escaping CellConfigurator) {
self.cellConfigurator = cellConfigurator
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return models.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let model = models[indexPath.item]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: model.reuseIdentifier, for: indexPath)
cellConfigurator(model, cell)
return cell
}
}
Notice the following changes
final class CollectionViewDataSource<Model>: NSObject, UICollectionViewDataSource where Model: FeedRenderable {
....
init(_ cellConfigurator: @escaping CellConfigurator)
....
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: model.reuseIdentifier, for: indexPath)
....
You can then ensure whatever model is passed in sets its reuseIdentifier
based on the item.type
property
extension GenericFeedItem: FeedRenderable {
var reuseIdentifier: String {
switch type {
case .news: return "FEED_ARTICLE_CELL"
case .alert: return "FEED_ALERT_CELL"
}
}
}
Your extension then becomes
extension CollectionViewDataSource where Model == GenericFeedItem {
static func make() -> CollectionViewDataSource {
return CollectionViewDataSource() { item, cell in
(cell as? FeedArticleCell)?.render(with: item)
(cell as? FeedAlertCell)?.render(with: item)
}
}
}