在开发任务中,我往往会需要实现如下功能。
需求如下:
1、我在一个ViewController中添加一个tableView,这个tableView能实现简单的路由跳转,而不是根据index去跳转
2、我还有一个UITableViewController,也要实现1中提到的一致的功能。
这两个需求中提到,ViewController和UITableViewController其实是作为父类在后续中使用的。
其实只用ViewController+tableView可以完全规避问题,但我就是要去占UITableViewController的一些默认好处。
那么,我以往的思路都是,抽象一个protocol,然后提供默认实现。
菜单项结构体如下:
/// 通用菜单项结构体
public struct HCMenuItem {
public let title: String
public let viewControllerType: UIViewController.Type
public init(title: String, vcType: UIViewController.Type) {
self.title = title
self.viewControllerType = vcType
}
}
protocol的大致代码如下:
/// 协议:声明具备「菜单项列表」能力的视图控制器应实现的行为
public protocol HCMenuTableDisplaying {
var menuItems: [HCMenuItem] { get set }
var cellIdentifier: String { get }
}
// MARK: - 为那些同时遵循 HCMenuTableDisplaying 协议 并且 是 UIViewController(或其子类)的类型,提供默认实现
public extension HCMenuTableDisplaying where Self: UIViewController {
// 默认 cell 标识符
var cellIdentifier: String {
return "HCMenuCell"
}
func hc_registerCellIdentifier(_ tableView: UITableView) {
tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier)
}
func hc_tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menuItems.count
}
func hc_TableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
let item = menuItems[indexPath.row]
cell.textLabel?.text = item.title
cell.accessoryType = .disclosureIndicator
return cell
}
func hc_TableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = menuItems[indexPath.row]
let viewController = item.viewControllerType.init()
//这句话很有意思,因为Self是UIViewController,所以可以用到navigationController
navigationController?.pushViewController(viewController, animated: true)
tableView.deselectRow(at: indexPath, animated: true)
}
}
具体的tableViewController在使用时做如下配置
class HCMenuTableViewController: UITableViewController {
var menuItems: [HCMenuItem] = []
override func viewDidLoad() {
hc_registerCellIdentifier(tableView)
}
}
extension HCMenuTableViewController: HCMenuTableDisplaying {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
hc_tableView(tableView, numberOfRowsInSection: section)
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = hc_TableView(tableView, cellForRowAt: indexPath)
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
hc_TableView(tableView, didSelectRowAt: indexPath)
}
}
这是一种常规思路。
下面提供一种更优雅的,“分离数据源”的思路以及实现。
核心思想是:把“菜单数据源/代理逻辑”抽离成一个独立对象(DataSource/Delegate 对象),由 VC 持有并桥接到 tableView
我们设置一个独立的数据源对象,用于驱动菜单型UITableView,让后让他去支持UITableViewDataSource和UITableViewDelegate两个协议
import UIKit
/// 独立的数据源对象,用于驱动菜单型UITableView
public class HCMenuTableViewDataSource: NSObject {
public var menuItems: [HCMenuItem] = []
public let cellIdentifier: String
/// 用户点击菜单项时的回调
/// - Parameter viewController: 将要被 push 的新视图控制器实例
public var onMenuItemSelected: ((UIViewController) -> Void)?
public init(cellIdentifier: String = "HCMenuCell") {
self.cellIdentifier = cellIdentifier
super.init()
}
}
extension HCMenuTableViewDataSource: UITableViewDataSource {
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return menuItems.count
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
let item = menuItems[indexPath.row]
cell.textLabel?.text = item.title
cell.accessoryType = .disclosureIndicator
return cell
}
}
extension HCMenuTableViewDataSource: UITableViewDelegate {
public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = menuItems[indexPath.row]
let viewController = item.viewControllerType.init()
onMenuItemSelected?(viewController)
tableView.deselectRow(at: indexPath, animated: true)
}
}
具体的tableViewController在使用时做如下配置
import UIKit
class HCMenuTableViewController: UITableViewController {
public var menuItems: [HCMenuItem] = [] {
didSet {
dataSource.menuItems = menuItems
tableView.reloadData()
}
}
private lazy var dataSource = HCMenuTableViewDataSource()
override func viewDidLoad() {
super.viewDidLoad()
setupDataSource()
}
private func setupDataSource() {
// 注册默认 cell
tableView.register(UITableViewCell.self, forCellReuseIdentifier: dataSource.cellIdentifier)
tableView.dataSource = dataSource
tableView.delegate = dataSource
// 设置点击回调
dataSource.onMenuItemSelected = { [weak self] viewController in
self?.navigationController?.pushViewController(viewController, animated: true)
}
}
}
假设你有一个类去继承HCMenuTableViewController,那么只需要如下配置
class MenuViewController: HCMenuViewController {
override func viewDidLoad() {
super.viewDidLoad()
menuItems = [
HCMenuItem(title: "MetalKit_原项目", vcType: ColorGridViewController.self),
HCMenuItem(title: "Test1", vcType: UIViewController.self),
HCMenuItem(title: "Test2", vcType: UIViewController.self),
HCMenuItem(title: "Test3", vcType: UIViewController.self),
]
}
}
这种分离数据源的思路,会比一开始抽离出一个protocol更为合理和优雅,毕竟结构体中去做默认实现,提供一些额外的默认实现方法,感觉起来怪怪的。protocol更正确的用法还是提供其他类必须遵守的属性和方法,而不是类似上面的利用默认实现给其他类提供公用的方法。
而分离数据源的思路,就看起来正常很多,HCMenuTableViewDataSource符合单一职责设计思路,在做他该做的事情。