Skip to content

Instantly share code, notes, and snippets.

@jakehawken
Created November 25, 2022 23:59
Show Gist options
  • Save jakehawken/9c5cac90d5889e854808ee5c7335f2db to your computer and use it in GitHub Desktop.
Save jakehawken/9c5cac90d5889e854808ee5c7335f2db to your computer and use it in GitHub Desktop.
A type for managing and encapsulating UITableViews with a more declarative interface, and without leaky encapsulation
// TableViewComposer.swift
// Created by Jacob Hawken on 5/28/22.
import UIKit
class TableViewComposer<T, E:Error>: NSObject {
struct Section {
let title: String?
let items: [T]
}
fileprivate let dataSource = TableViewDataSource()
fileprivate let delegate = TableViewDelegate()
fileprivate var sections: [Section]
let tableView: UITableView
let cellTypes: [UITableViewCell.Type]
init(cellTypes: [UITableViewCell.Type], sections: [Section] = []) {
self.cellTypes = cellTypes
self.sections = sections
let tv = UITableView(frame: .zero)
cellTypes.forEach { tv.registerCellType($0) }
tv.dataSource = dataSource
tv.delegate = delegate
tableView = tv
super.init()
dataSource.numberOfSections = { [weak self] _ -> Int in
let count = self?.sections.count
return count ?? 0
}
dataSource.numberOfRowsInSection = { [weak self] _, sectionIndex -> Int in
self?.sections[sectionIndex].items.count ?? 0
}
dataSource.titleForHeaderInSection = { [weak self] _, sectionIndex -> String? in
self?.sections[sectionIndex].title
}
}
func refreshWithData(_ newSections: [Section]) {
sections = newSections
tableView.reloadData()
}
}
// MARK: - Configuration/Declaration interface
extension TableViewComposer {
@discardableResult func cellForIndexPathWithItem(_ implementation: @escaping (UITableView, IndexPath, T) -> UITableViewCell) -> Self {
dataSource.cellForRowAt = { [weak self] tableView, indexPath -> UITableViewCell in
guard let item = self?.sections[indexPath.section].items[indexPath.row] else {
tableView.registerCellType(UITableViewCell.self)
return tableView.dequeueCell(UITableViewCell.self, indexPath: indexPath)
}
return implementation(tableView, indexPath, item)
}
return self
}
@discardableResult func canEditRowAtIndexPathWithItem(_ implementation: @escaping (UITableView, IndexPath, T) -> Bool) -> Self {
dataSource.canEditRow = { [weak self] tableView, indexPath -> Bool in
guard let item = self?.sections[indexPath.section].items[indexPath.row] else {
return false
}
return implementation(tableView, indexPath, item)
}
return self
}
@discardableResult func commitEditingStyleAtIndexPathWithItem(_ implementation: @escaping (UITableView, UITableViewCell.EditingStyle, IndexPath, T) -> Void) -> Self {
dataSource.commitEditingStyle = { [weak self] tableView, editingStyle, indexPath in
guard let item = self?.sections[indexPath.section].items[indexPath.row] else {
return
}
implementation(tableView, editingStyle, indexPath, item)
}
return self
}
@discardableResult func didSelectRowWithItem(_ implementation: @escaping (UITableView, IndexPath, T) -> Void) -> Self {
delegate.didSelectRow = { [weak self] tableView, indexPath in
guard let item = self?.sections[indexPath.section].items[indexPath.row] else {
return
}
implementation(tableView, indexPath, item)
}
return self
}
@discardableResult func heightForRowWithItem(_ implementation: @escaping (UITableView, IndexPath, T) -> CGFloat) -> Self {
delegate.heightForRow = { [weak self] tableView, indexPath -> CGFloat in
guard let item = self?.sections[indexPath.section].items[indexPath.row] else {
return 0
}
return implementation(tableView, indexPath, item)
}
return self
}
@discardableResult func heightForHeaderInSection(_ implementation: @escaping (UITableView, Int, Section) -> CGFloat) -> Self {
delegate.heightForHeaderInSection = { [weak self] tableView, sectionIndex -> CGFloat in
guard let section = self?.sections[sectionIndex] else {
return 0
}
return implementation(tableView, sectionIndex, section)
}
return self
}
@discardableResult func heightForFooterInSection(_ implementation: @escaping ((UITableView, Int, Section) -> CGFloat)) -> Self {
delegate.heightForFooterInSection = { [weak self] tableView, sectionIndex -> CGFloat in
guard let section = self?.sections[sectionIndex] else {
return 0
}
return implementation(tableView, sectionIndex, section)
}
return self
}
}
// MARK: - private helpers & types
extension UITableView {
var registeredClasses: [String: Any] {
let dict = value(forKey: "_cellClassDict") as? [String: Any]
return dict ?? [:]
}
func reuseIdForCellType(_ cellType: UITableViewCell.Type) -> String {
String(describing: cellType)
}
func registerCellType(_ cellType: UITableViewCell.Type) {
if hasRegisteredClass(cellType) {
return
}
let reuseID = reuseIdForCellType(cellType)
register(cellType, forCellReuseIdentifier: reuseID)
}
func hasRegisteredClass(_ cellClass: UITableViewCell.Type) -> Bool {
let registered = registeredClasses
return registered.values.contains { value -> Bool in
guard let cellType = value as? UITableView.Type else {
return false
}
return cellType == cellClass
}
}
func dequeueCell<T: UITableViewCell>(_ cellType: T.Type = T.self, indexPath: IndexPath) -> T {
let reuseID = reuseIdForCellType(cellType)
guard let cell = dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as? T else {
Logger.crashError("A cell of type \(cellType) could not be dequeued for this TableView.")
}
return cell
}
func dequeueCell<T: UITableViewCell>(_ cellType: T.Type = T.self) -> T {
let reuseID = reuseIdForCellType(cellType)
guard let cell = dequeueReusableCell(withIdentifier: reuseID) as? T else {
Logger.crashError("A cell of type \(cellType) could not be dequeued for this TableView.")
}
return cell
}
}
// MARK: - UITableView DataSource/Delegate
class TableViewDataSource: NSObject, UITableViewDataSource {
var numberOfSections: ((UITableView) -> Int)?
var numberOfRowsInSection: ((UITableView, Int) -> Int)?
var cellForRowAt: ((UITableView, IndexPath) -> UITableViewCell)?
var titleForHeaderInSection: ((UITableView, Int) -> String?)?
var canEditRow: ((UITableView, IndexPath) -> Bool)?
var commitEditingStyle: ((UITableView, UITableViewCell.EditingStyle, IndexPath) -> Void)?
func numberOfSections(in tableView: UITableView) -> Int {
numberOfSections?(tableView) ?? 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
numberOfRowsInSection?(tableView, section) ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cellForRowAt = cellForRowAt else {
Logger.crashError("Implementation for \(String(describing: UITableViewDataSource.tableView(_:cellForRowAt:))) not provided to instance of \(TableViewDataSource.self).")
}
return cellForRowAt(tableView, indexPath)
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
titleForHeaderInSection?(tableView, section)
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
canEditRow?(tableView, indexPath) ?? false
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
commitEditingStyle?(tableView, editingStyle, indexPath)
}
}
class TableViewDelegate: NSObject, UITableViewDelegate {
var didSelectRow: ((UITableView, IndexPath) -> Void)?
var heightForRow: ((UITableView, IndexPath) -> CGFloat)?
var heightForHeaderInSection: ((UITableView, Int) -> CGFloat)?
var heightForFooterInSection: ((UITableView, Int) -> CGFloat)?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
didSelectRow?(tableView, indexPath)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
heightForRow?(tableView, indexPath) ?? UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
heightForHeaderInSection?(tableView, section) ?? UITableView.automaticDimension
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
heightForFooterInSection?(tableView, section) ?? UITableView.automaticDimension
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment