The UITableView and UICollectionView classes serve table-like and matrix-like layout for content to be displayed on screen. Both components display their data in corresponding UITableViewCell and UICollectionViewCell subclasses. Both components contain mechanisms for reusing a cell, that was initialised earlier and is not visible on the screen, e.g. when user scrolled the content. You can find a nice description and visualisations on that subject here.

Reusing a UITableViewCell - old way

In order to reuse a cell in a tableView, the cell has to be registered for reuse with an identifier. Usually it is done at the time you configure your views, e.g. in viewDidLoad.

lazy var tableView: UITableView = UITableView(frame: .zero, style: .plain)

override func viewDidLoad() {
	super.viewDidLoad()
	tableView.register(MyCell.self, forCellReuseIdentifier: "MyCell")
}

To reuse a cell it has to be dequeued from a tableView. Dequeueing is done by dequeueReusableCell(withIdentifier:for:) method. It dequeues a cell or initialises a new one if none is queued for reuse.

 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
	guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyCell", for: indexPath)
	as? MyCell else { return UITableViewCell() }
	//Configure cell
}

Have you noticed the "MyCell" duplicated in viewDidLoad and tableView(cellForRowAt:)? It felt ugly to me so I have decided to refactor that code, when I repeated it in a few view controllers.

I refactored the code so that a cell I registered for reuse from a stored cellClass and a computed property cellIdentifier.

let cellClass: AnyClass = MyCell.self
var cellIdentifier: String { return String(describing: cellClass) }

override func viewDidLoad() {
	super.viewDidLoad()
	tableView.register(self.cellClass, forCellReuseIdentifier: self.cellIdentifier)
}

Actually, this approach is kinda nice if we have a few cells to be registered for reuse. We can do some functional magic 🔮, like this:


let cellClasses: [AnyClass] = [MyCell.self, MyOtherCell.self, UITableViewCell.self]
var cellIdentifiers: [String] { return cellClasses.map{ String(describing: $0) } }

override func viewDidLoad() {
	super.viewDidLoad()
	let reuse = zip(cellClasses, cellIdentifiers)
	for (cell, cellIdentifier) in reuse {
		tableView.register(cell, forCellReuseIdentifier: cellIdentifier)
	}
}

The cellIdentifier property is of course used in tableView(cellForRowAt:). What I still don’t like in this code is that I still have to cast the reused cell to an appropriate type, even though I stored a cellClass 😭.

 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
	guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
	as? MyCell else { return UITableViewCell() }
	//Configure cell
}

Finally, my colleague Alek has invented a great solution to my problem!

Extending UITableViewCell

First of all, it is a tedious work to write cellIdentifier property in every view controller. Why not to extend the UITableViewCell?

public extension UITableViewCell {
    public static var identifier: String { return String(describing: self) }
}

Then we can further extend a UITableView with a new version of register method to make use of identifier property on a UITableViewCell.

public extension UITableView {
    public func register(_ cell: UITableViewCell.Type) {
        register(cell, forCellReuseIdentifier: cell.identifier)
    }

But the most beautiful magic 🔮 happens when we extend UITableView with new version of dequeueReusableCell(...) method! It uses generics, takes as an argument a class of a cell, an indexPath and a configure closure to which a cell of desired type (i.e. of CellClass.Type) is passed. No more guard and casting in tableView(cellForRowAt:)! Yuppii 😊!

	public func dequeueReusableCell<CellClass: UITableViewCell>(of
		class: CellClass.Type,
		for indexPath: IndexPath,
		configure: ((CellClass) -> Void) = { _ in }) -> UITableViewCell {
			let cell = dequeueReusableCell(withIdentifier: CellClass.identifier, for: indexPath)

			if let typedCell = cell as? CellClass {
				configure(typedCell)
			}
			
	    return cell
	}
}

So now we can easily dequeue a cell of an appropriate type! How neat is that 😀 !?

let cellClass = MyCell.self
//... register a cell for reuse
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
	return tableView.dequeueReusableCell(of: cellClass, for: IndexPath(row: 0, section: 0)) { cell in
		cell.textLabel?.text = indexPath.description
	}
}

Wrap up

The presented approach simplifies cell registration for reuse and allows to avoid guard-cell-casting. The same approach can be used for UICollectionView.

What you cannot do with the presented solution is taking cellClass from an array, because a compiler doesn’t know type at compile time.

class TableViewDataSource: NSObject, UITableViewDataSource {
    let cellClasses: [AnyClass] = [MyCell.self, UITableViewCell.self]
    func numberOfSections(in tableView: UITableView) -> Int {
        return 3
    }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return tableView.dequeueReusableCell(of: cellClasses[indexPath.section], for: indexPath) { cell in
        //ERROR: Cannot convert value of type '()?' to closure result type 'Void' (aka '()')
            cell.textLabel?.text = indexPath.description
        }
    }
}

What you can do with that is of course using a switch statement, e.g. over section, to infer a cell type.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
	switch indexPath.section {
		case 0:
			return tableView.dequeueReusableCell(of: MyCell.self, for: indexPath) { cell in
				cell.textLabel?.text = indexPath.description
      }
		default:
			return tableView.dequeueReusableCell(of: UITableViewCell.self, for: indexPath) { cell in
				cell.textLabel?.text = indexPath.description
			}
	}
}

You can play with the solution by downloading our playgrounds or by grabbing gists from links section.