Skip to content

Instantly share code, notes, and snippets.

@emoonadev
Last active November 16, 2021 14:31
Show Gist options
  • Save emoonadev/a907cd2304a538e399ac45c17cf8d904 to your computer and use it in GitHub Desktop.
Save emoonadev/a907cd2304a538e399ac45c17cf8d904 to your computer and use it in GitHub Desktop.
TableViewDataSourceProvider
protocol ObservableSearchBar {
var textDidChange: Observable<String> { get }
var text: String? { get set }
}
class TableViewDataSourceProvider<Item, Cell: UITableViewCell & NibLoadableView>: NSObject, UITableViewDataSource, UITableViewDelegate, UISearchResultsUpdating {
typealias CellConfigurator = (_ indexPath: IndexPath, _ item: Item, _ cell: Cell) -> Void
typealias HeightConfigurator = (_ indexPath: IndexPath, _ item: Item) -> CGFloat
typealias ContextMenuConfigurator = (_ indexPath: IndexPath, _ item: Item) -> UIMenu
typealias SwipeConfigurator = (_ rowAction: UIContextualAction, _ item: Item) -> Void
typealias SimpleConfigurator = (_ items: [Item], _ filteredItems: [Item], _ indexPath: IndexPath, _ item: Item) -> Void
private var items: [Item]
private let cell: Cell.Type
private var isCellRegistered = false
private let cellConfigurator: CellConfigurator
private var heightConfigurator: HeightConfigurator?
private var didSelectedItemAt: SimpleConfigurator?
private var contextMenu: ContextMenuConfigurator?
private var destructiveSwipe: (String, SwipeConfigurator)?
private let isAutomaticDimension: Bool
private var registeredSearchBar: (searchBar: ObservableSearchBar, keyPath: KeyPath<Item, String>)?
private var registeredSearchController: (searchController: UISearchController, keyPath: KeyPath<Item, String>)?
private var registeredTableView: UITableView?
var filteredItems = [Item]()
init(items: [Item], cell: Cell.Type, isAutomaticDimension: Bool = false, cellConfigurator: @escaping CellConfigurator) {
self.items = items
self.cell = cell
self.isAutomaticDimension = isAutomaticDimension
self.cellConfigurator = cellConfigurator
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if !isCellRegistered {
isCellRegistered = true
tableView.register(Cell.self)
registeredTableView = tableView
}
return isFiltering() ? filteredItems.count : items.count
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let model = items[indexPath.row]
let cell = loadNIB()
return heightConfigurator?(indexPath, model) ?? (isAutomaticDimension ? UITableView.automaticDimension : cell.frame.height)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
didSelectedItemAt?(items, filteredItems, indexPath, items[indexPath.row])
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = isFiltering() ? filteredItems[indexPath.row] : items[indexPath.row]
let cell: Cell = tableView.dequeueReusableCell(forIndexPath: indexPath)
cellConfigurator(indexPath, model, cell)
return cell
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
var actions: [UIContextualAction] = []
if let destructive = destructiveSwipe {
let destructive = UIContextualAction(style: .destructive, title: destructive.0) { action, _, _ in
let model = self.isFiltering() ? self.filteredItems[indexPath.row] : self.items[indexPath.row]
destructive.1(action, model)
}
actions.append(destructive)
}
return UISwipeActionsConfiguration(actions: actions)
}
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
let model = isFiltering() ? filteredItems[indexPath.row] : items[indexPath.row]
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: { _ in
self.contextMenu?(indexPath, model)
})
}
private func loadNIB() -> Cell {
return Bundle(for: Cell.self as AnyClass).loadNibNamed(String(describing: Cell.self), owner: nil, options: nil)![0] as! Cell
}
public func updateSearchResults(for searchController: UISearchController) {
guard let keyPath = registeredSearchController?.keyPath, let searchText = searchController.searchBar.text?.lowercased() else { return }
filterTableView(by: keyPath, and: searchText)
}
}
// MARK: - Configurators
extension TableViewDataSourceProvider {
func heightForRow(config: @escaping HeightConfigurator) -> Self {
heightConfigurator = config
return self
}
func didSelectedItemAt(config: @escaping SimpleConfigurator) -> Self {
didSelectedItemAt = config
return self
}
func contextMenu(config: @escaping ContextMenuConfigurator) -> Self {
contextMenu = config
return self
}
func destructiveSwipe(title: String, config: @escaping SwipeConfigurator) -> Self {
destructiveSwipe = (title, config)
return self
}
func reloadData(with items: [Item]) {
self.items = items
registeredTableView?.reloadData()
}
private func filterTableView(by keyPath: KeyPath<Item, String>, and searchText: String) {
filteredItems = items.filter { $0[keyPath: keyPath].lowercased().contains(searchText.lowercased()) }
registeredTableView?.reloadData()
}
}
// MARK: - Filter logic
extension TableViewDataSourceProvider {
func registerSearchBar(_ searchBar: (searchBar: ObservableSearchBar, keyPath: KeyPath<Item, String>)) -> TableViewDataSourceProvider {
registeredSearchBar = searchBar
searchBar.searchBar.textDidChange.observe(on: self) { _, searchText in
guard let keyPath = self.registeredSearchBar?.keyPath else { return }
self.filterTableView(by: keyPath, and: searchText)
}
return self
}
func registerSearchController(_ searchController: (searchController: UISearchController, keyPath: KeyPath<Item, String>)) -> TableViewDataSourceProvider {
registeredSearchController = searchController
registeredSearchController?.searchController.searchResultsUpdater = self
return self
}
func searchBarIsEmpty() -> Bool {
registeredSearchBar?.searchBar.text?.isEmpty ?? registeredSearchController?.searchController.searchBar.text?.isEmpty ?? true
}
func isFiltering() -> Bool { !searchBarIsEmpty() }
}
// Nib Loadable vview
protocol ReusableView: AnyObject {
static var defaultReuseIdentifier: String { get }
}
extension ReusableView where Self: UIView {
static var defaultReuseIdentifier: String {
return NSStringFromClass(self)
}
}
protocol NibLoadableView: AnyObject {
static var nibName: String { get }
}
extension NibLoadableView where Self: UIView {
static var nibName: String {
return NSStringFromClass(self).components(separatedBy: ".").last!
}
}
// MARK: - Collection view
extension UICollectionView {
func register<T: UICollectionViewCell>(_: T.Type) {
register(T.self, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
}
func register<T: UICollectionViewCell>(_: T.Type) where T: NibLoadableView {
let bundle = Bundle(for: T.self)
let nib = UINib(nibName: T.nibName, bundle: bundle)
register(nib, forCellWithReuseIdentifier: T.defaultReuseIdentifier)
}
func dequeueReusableCell<T: UICollectionViewCell>(forIndexPath indexPath: IndexPath) -> T {
guard let cell = dequeueReusableCell(withReuseIdentifier: T.defaultReuseIdentifier, for: indexPath) as? T else {
fatalError("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
}
return cell
}
}
extension UICollectionViewCell: ReusableView {}
// MARK: - TableView
extension UITableView {
func register<T: UITableViewCell>(_: T.Type) {
register(T.self, forCellReuseIdentifier: T.defaultReuseIdentifier)
}
func register<T: UITableViewCell>(_: T.Type) where T: NibLoadableView {
let bundle = Bundle(for: T.self)
let nib = UINib(nibName: T.nibName, bundle: bundle)
register(nib, forCellReuseIdentifier: T.defaultReuseIdentifier)
}
func dequeueReusableCell<T: UITableViewCell>(forIndexPath indexPath: IndexPath) -> T {
guard let cell = dequeueReusableCell(withIdentifier: T.defaultReuseIdentifier, for: indexPath) as? T else {
fatalError("Could not dequeue cell with identifier: \(T.defaultReuseIdentifier)")
}
return cell
}
}
extension UITableViewCell: ReusableView {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment