UIKit’s UITableViewCell/UICollectionViewCell …my thoughts on subclassing them
I think it should be reconsidered.
Many a times I come across UITableViewCell/UICollectionViewCell subclasses which, in my opinion, seem too niche of an implementation. It needs to abstracted.
Requirement:
To display, say, a collection view in a row of a table view.
Widely implemented solution:
Create a UITableViewCell sublclass and add a collection view to it:
// most solutions
class ItemListCell: UITableViewCell {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) var list: [String] = [] init(style: UITableViewCell.CellStyle, reuseIdentifier: String?, list: [String]) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.list = list
}
override func layoutSubviews() {
// add the collectionview and cover the cell with it
collectionView.translatesAutoresizingMaskIntoConstraints = true
contentView.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
collectionView.topAnchor.constraint(equalTo: contentView.topAnchor),
collectionView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
}
}
Re-evaluate:
Now, what if we want to move this view to be implemented as UICollectionViewCell, or even a view backing a UIViewController?!
Would be odd to set a UIViewController’s view or a UICollectionViewCell’s subview to be a UITableViewCell 🤢!
The better solution would be to implement ItemList to be a UIView subclass…
class ItemListView: UIView {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
var list: [String] = []
init(list: [String]) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.list = list
}
override func layoutSubviews() {
// add the collectionView as shown before
}
}
…and manipulate its layout, based on where it is used (below for when used in as a cell):
let listView = ItemListView(list: ["item 1", "item 2"])
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell ID", for: indexPath)
listView.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(listView)
NSLayoutConstraint.activate([
// ... constraints as before
])
return cell
}
BONUS:
We could make use of templates to add a beautiful method as a UITableView extension, to return us a cell that will hold our custom view:
extension UITableView {
/// dequeues and returns a cell and the view as a tuple.
/// Adds the UIView subclass if it exists for the given tag, or the view returned in the `constructView` closure.
func dequeueCell<T: UIView>(withIdentifier identifier: String, indexPath: IndexPath, tag: Int, constructView: () -> T) -> (UITableViewCell, T) {
let cell = dequeueReusableCell(withIdentifier: identifier, for: indexPath)
// try to find if the view exists for the given tag.
// If it doesn't, add the view returned by calling the create instance block
let view = cell.viewWithTag(tag) as? T ?? constructView()
// set the tag of this view (existing or created instance) with the tag given
view.tag = tag
// add the constraints to add the view to the cell
view.translatesAutoresizingMaskIntoConstraints = false
cell.contentView.addSubview(view)
NSLayoutConstraint.activate([
view.leftAnchor.constraint(equalTo: cell.contentView.leftAnchor),
view.rightAnchor.constraint(equalTo: cell.contentView.rightAnchor),
view.topAnchor.constraint(equalTo: cell.contentView.topAnchor),
view.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor)
])
return (cell, view)
}
}
Here we make use of the tag property of views to check if the the cell already contains a view instance of our custom class.
If it doesn’t, then we can create the view using the constructView closure.
The construct view block enables the caller to use a custom init (`init(list: [String])`of our ItemListView class, in our case) of the desired class, and then return it:
let cellID = "cell ID"
let itemListTag = 2
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// our newly created extension in our UITableViewDataSource method
let (cell, view) = tableView.dequeueCell(withIdentifier: cellID, indexPath: indexPath, tag: itemListTag) {
ItemListView(list: ["item 1", "item 2"])
}
// configure your custom view instance `view`, if needed
return cell
}
Thanks for reading!