关于tableview分离数据源的一种思路和实现

169 阅读3分钟

在开发任务中,我往往会需要实现如下功能。

需求如下:
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符合单一职责设计思路,在做他该做的事情。