UIKit’s UITableViewCell/UICollectionViewCell …my thoughts on subclassing them

Vijay Chandran Jayachandran
3 min readJan 29, 2023

--

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!

--

--