Skip to content

Instantly share code, notes, and snippets.

@srstanic
Last active June 22, 2020 11:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save srstanic/0a3bd99053cf6f7506693a3f9b9c1206 to your computer and use it in GitHub Desktop.
Save srstanic/0a3bd99053cf6f7506693a3f9b9c1206 to your computer and use it in GitHub Desktop.
Generic implementation of UITableViewDataSource & UITableViewDelegate
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