Last active
June 22, 2020 11:38
-
-
Save srstanic/0a3bd99053cf6f7506693a3f9b9c1206 to your computer and use it in GitHub Desktop.
Generic implementation of UITableViewDataSource & UITableViewDelegate
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
extension UITableViewCell { | |
class var reuseIdentifier: String { | |
return "tableViewCell" | |
} | |
} | |
extension Collection { | |
/// Returns the element at the specified index if it is within bounds, otherwise nil. | |
// https://stackoverflow.com/a/30593673/517865 | |
subscript (safe index: Index) -> Element? { | |
return indices.contains(index) ? self[index] : nil | |
} | |
} | |
class TableViewSection { | |
convenience init(rows: [TableViewRow]) { | |
self.init() | |
self.rows = rows | |
} | |
var rows: [TableViewRow] = [] | |
var headerHeight: CGFloat = 0 | |
var getViewForHeader: (UITableView, Int) -> UIView? = { tableView, sectionNumber in | |
return nil | |
} | |
var getViewForFooter: (UITableView, Int) -> UIView? = { tableView, sectionNumber in | |
return nil | |
} | |
} | |
typealias UntypedCellAndModelHandler = (UITableViewCell, Any?) -> Void | |
class TableViewRow { | |
convenience init<CellType, ModelType>(model: ModelType, cellConfigurator: @escaping (CellType, ModelType) -> Void) { | |
self.init() | |
self.model = model | |
setCellConfigurator(cellConfigurator) | |
} | |
convenience init<CellType>(cellConfigurator: @escaping (CellType) -> Void) { | |
self.init() | |
setCellConfigurator(cellConfigurator) | |
} | |
var rowHeight: CGFloat = 44 | |
var model: Any? | |
var cellType: UITableViewCell.Type = UITableViewCell.self { | |
didSet { | |
cellIdentifier = cellType.reuseIdentifier | |
} | |
} | |
var nibName: String? = nil | |
private (set) var cellIdentifier = UITableViewCell.reuseIdentifier | |
private (set) var configureCell: UntypedCellAndModelHandler = { cell, model in } | |
func setCellConfigurator<CellType, ModelType>(_ typedCellConfigurator: @escaping (CellType, ModelType) -> Void) -> Void { | |
configureCell = untypedFunction(typedCellConfigurator) | |
} | |
func setCellConfigurator<CellType>(_ typedCellConfigurator: @escaping (CellType) -> Void) -> Void { | |
configureCell = untypedFunction(typedCellConfigurator) | |
} | |
private (set) var handleCellSelected: (UITableViewCell, Any?) -> Void = { cell, model in } | |
func setCellSelectionHandler<CellType, ModelType>(_ typedCellSelectionHandler: @escaping (CellType, ModelType) -> Void) -> Void { | |
handleCellSelected = untypedFunction(typedCellSelectionHandler) | |
} | |
func setCellSelectionHandler<CellType>(_ typedCellSelectionHandler: @escaping (CellType) -> Void) -> Void { | |
handleCellSelected = untypedFunction(typedCellSelectionHandler) | |
} | |
private func untypedFunction<CellType>(_ typedFunction: @escaping (CellType) -> Void) -> UntypedCellAndModelHandler { | |
let untypedFunction: UntypedCellAndModelHandler = { cell, _ in | |
guard | |
let customCell = cell as? CellType | |
else { | |
return | |
} | |
typedFunction(customCell) | |
} | |
return untypedFunction | |
} | |
private func untypedFunction<CellType, ModelType>(_ typedFunction: @escaping (CellType, ModelType) -> Void) -> UntypedCellAndModelHandler { | |
let untypedFunction: UntypedCellAndModelHandler = { cell, model in | |
guard | |
let customCell = cell as? CellType, | |
let customModel = model as? ModelType | |
else { | |
return | |
} | |
typedFunction(customCell, customModel) | |
} | |
return untypedFunction | |
} | |
} | |
class TableViewModel: NSObject, UITableViewDataSource, UITableViewDelegate { | |
var sections: [TableViewSection] = [] | |
/// Goes through all rows and registers nibs, if present, or cell classes, if nibs are not present, | |
/// with the tableview | |
func registerCells(with tableView: UITableView) { | |
var reuseIdentifierToNibOrClassNameMap: [String: (nibName: String?, type: UITableViewCell.Type)] = [:] | |
let allRows: [TableViewRow] = sections.flatMap({ $0.rows }) | |
for row in allRows { | |
reuseIdentifierToNibOrClassNameMap[row.cellIdentifier] = (nibName: row.nibName, type: row.cellType) | |
} | |
for (reuseIdentifier, nibNameOrClass) in reuseIdentifierToNibOrClassNameMap { | |
if let nibName = nibNameOrClass.nibName { | |
let nib = UINib(nibName: nibName, bundle: nil) | |
tableView.register(nib, forCellReuseIdentifier: reuseIdentifier) | |
} else { | |
let cellType = nibNameOrClass.type | |
tableView.register(cellType, forCellReuseIdentifier: reuseIdentifier) | |
} | |
} | |
} | |
func indexPath(for rowToGetIndexPathFor: TableViewRow) -> IndexPath? { | |
var sectionIndex = 0 | |
for section in sections { | |
var rowIndex = 0 | |
for row in section.rows { | |
if row === rowToGetIndexPathFor { | |
return IndexPath(row: rowIndex, section: sectionIndex) | |
} | |
rowIndex += 1 | |
} | |
sectionIndex += 1 | |
} | |
return nil | |
} | |
// MARK: UITableViewDataSource | |
func numberOfSections(in tableView: UITableView) -> Int { | |
return sections.count | |
} | |
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { | |
return sections[safe: section]?.rows.count ?? 0 | |
} | |
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { | |
let defaultCell = UITableViewCell() | |
guard let row = getRow(for: indexPath) else { | |
return defaultCell | |
} | |
let cell = tableView.dequeueReusableCell(withIdentifier: row.cellIdentifier, for: indexPath) | |
row.configureCell(cell, row.model) | |
return cell | |
} | |
// MARK: UITableViewDelegate | |
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { | |
return sections[safe: section]?.headerHeight ?? 0 | |
} | |
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { | |
return getRow(for: indexPath)?.rowHeight ?? 0 | |
} | |
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { | |
return sections[safe: section]?.getViewForHeader(tableView, section) | |
} | |
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { | |
return sections[safe: section]?.getViewForFooter(tableView, section) | |
} | |
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { | |
guard | |
let selectedCell = tableView.cellForRow(at: indexPath), | |
let row = getRow(for: indexPath) | |
else { | |
return | |
} | |
row.handleCellSelected(selectedCell, row.model) | |
} | |
private func getSection(for indexPath: IndexPath) -> TableViewSection? { | |
return sections[safe: indexPath.section] | |
} | |
private func getRow(for indexPath: IndexPath) -> TableViewRow? { | |
return sections[safe: indexPath.section]?.rows[safe: indexPath.row] | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment