2

UITableView was build when objc was already 'old' and swift was never mentioned. In objc there was no need if you dequeued a cell to cast it, you just assigned it and everything went right as long as you had the correct cell.

With swift the power of generics came to the iOS (and other apple) platform(s).

Now I find myself writing a lot of boilerplate again and again (define an identifier, cast the cell, use force unwrap of fatal error,..).

So I wondered if there are ideas that could make this easier to use and cleaner in the code.

Saren Inden
  • 3,450
  • 4
  • 32
  • 45

2 Answers2

3

An easy way to solve this is by writing a small extension.

This solution will result in a fatalError if a cell is dequeued that has not been registered. This is already the default behaviour for iOS if we call dequeueReusableCell(withIdentifier:for:) if we have not registered the cell.

To let this work we need a way to create a unique identifier for any type of cell that we will register and dequeue using generics. If you would want to have a way to dequeue the same cell for different identifiers then you will have to fallback to the default system (never had any need for that).

So lets make a class named UITableView+Tools.swift (or what ever you like to name it).

extension UITableView {
    private func reuseIndentifier<T>(for type: T.Type) -> String {
        return String(describing: type)
    }

    public func register<T: UITableViewCell>(cell: T.Type) {
        register(T.self, forCellReuseIdentifier: reuseIndentifier(for: cell))
    }

    public func register<T: UITableViewHeaderFooterView>(headerFooterView: T.Type) {
        register(T.self, forHeaderFooterViewReuseIdentifier: reuseIndentifier(for: headerFooterView))
    }

    public func dequeueReusableCell<T: UITableViewCell>(for type: T.Type, for indexPath: IndexPath) -> T {
        guard let cell = dequeueReusableCell(withIdentifier: reuseIndentifier(for: type), for: indexPath) as? T else {
            fatalError("Failed to dequeue cell.")
        }

        return cell
    }

    public func dequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView>(for type: T.Type) -> T {
        guard let view = dequeueReusableHeaderFooterView(withIdentifier: reuseIndentifier(for: type)) as? T else {
            fatalError("Failed to dequeue footer view.")
        }

        return view
    }
}

So now all we have to do in our class (i.e. view controller) is register the cell (no identifier needed) and dequeue it(no identifier, no casting, force unwrapping or manual unwrap with a guard)

func viewDidLoad {
    ...

    tableView.register(MyCustomCell.self)
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 
    let cell = dequeueReusableCell(forType: MyCustomCell.self, for: indexPath)
    cell.viewModel = cellModel(for: indexPath)

    return cell
}

And that's it. Hope you like the idea. Any other (better or worse ) ideas are welcome.

Saren Inden
  • 3,450
  • 4
  • 32
  • 45
1

@Saren your solution worked fine for single UITableViewCell but I have done some enhancements on it which supports multiple UITableViewCell registrations.

UITableView+extension.swift

public protocol ClassNameProtocol {
    static var className: String { get }
    var className: String { get }
}

public extension ClassNameProtocol {
    public static var className: String {
        return String(describing: self)
    }

    public var className: String {
        return type(of: self).className
    }
}

extension NSObject: ClassNameProtocol {}


public extension UITableView {
    public func register<T: UITableViewCell>(cellType: T.Type) {
        let className = cellType.className
        let nib = UINib(nibName: className, bundle: nil)
        register(nib, forCellReuseIdentifier: className)
    }

    public func register<T: UITableViewCell>(cellTypes: [T.Type]) {
        cellTypes.forEach { register(cellType: $0) }
    }

    public func dequeueReusableCell<T: UITableViewCell>(with type: T.Type, for indexPath: IndexPath) -> T {
        return self.dequeueReusableCell(withIdentifier: type.className, for: indexPath) as! T
    }

    public func registerHeaderFooter<T: UITableViewHeaderFooterView>(HeaderFooterType: T.Type) {
        let className = HeaderFooterType.className
        let nib = UINib(nibName: className, bundle: nil)
        register(nib, forHeaderFooterViewReuseIdentifier: className)
    }

    public func registerHeaderFooter<T: UITableViewHeaderFooterView>(HeaderFooterTypes: [T.Type]) {
        HeaderFooterTypes.forEach { registerHeaderFooter(HeaderFooterType: $0) }
    }
}

Use like

fileprivate let arrCells = [ContactTableViewCell.self,ContactDetailsCell.self]
self.register(cellTypes: arrCells)

Note: Please ensure your class name and reusableidenfer of UITableviewCell should be the same.

Hitesh Surani
  • 12,733
  • 6
  • 54
  • 65