iOS 基础系统组件UITableViewController框架搭建设计与实现探究

2025.01.21 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

在设计base collection类的时候,顺便设计了一份base table,项目里已经有一份已经在用的base table框架了,考虑到取长补短的原因,在这里还是做一次技术设计与实现。 一次iOS组内分享会中分享自己的想法后,在这里把设计思路和源码放上来以作记录。

因为是一边学习Swift一边写的,有一些语法问题上还可以再做一步优化(比如,把一些逻辑! 强制解析 as! 类型改成可选型 as? 防止崩溃,不过使用 as! 也能更好帮助开发阶段调试定位问题),先在这里做大体框架的源码设计与实现分享。

这份一是基于 《阅读类APP》 的demo

一、设计思想与实际作用

1.统一代码开发规范,命名方式

首先,因为每个程序员思考方式不同,想法不同,写出来的代码、文件命名等等,自然会有不一样的地方。如果有一套统一的、能覆盖大多数场景的开发模板(base组件),将会大大减少新员工或者相同开发人员交替开发的阅读成本、开发成本

2.提高开发效率

有一套设计良好的基础组件模板,可以减少开发中一些基础 UITableViewController 功能的重复代码编写,也可以迅速的复用相似的UI层到相同的新页面中。

3.使代码更加内聚

如果能够把 UITableViewController 常用的功能封装后,一个构造函数完成以前需要好几个代理方法才能写完的功能,这样一个构造函数,对于开发、维护以及更新页面的时候就会变得特别方便。
既能把处理逻辑放到一起,也能让代码顺序对应页面元素顺序,大大提高了代码的可阅读性、可维护性、开发效率

二、框架搭建与源码实现

base TableViewController 组件
YYYBaseTableViewController

//  Created by Yimmm on 2022/6/2.
//  Copyright © 2022 xj. All rights reserved.
//  baseCollectionViewController组件

import UIKit

@objc protocol YYYTableViewDelegate: AnyObject {
    
    /**
     didSelect回调
     tableViewModel: 数据源TableViewModel
     didSelectCellModel: 点击Cell的CellModel
     */
    @objc optional func tableViewModel(_ tableViewModel: YYYBaseTableViewModel, didSelectCellModel: YYYBaseTableViewCellModel,didSelectRowAt indexPath: IndexPath)
    
    /**
     heightForRow回调
     tableViewModel: 数据源TableViewModel
     cellModel: 当前Cell的CellModel
     返回当前cell的行高(可自定义处理动态cell的高度)
     */
    @objc optional func tableViewModel(_ tableViewModel: YYYBaseTableViewModel, cellModel: YYYBaseTableViewCellModel, heightForRowAt indexPath: IndexPath) -> CGFloat
    
    /**
     viewForHeaderInSection回调
     tableViewModel: 数据源TableViewModel
     sectionModel: 当前secion的sectionModel
     返回section中的HeaderView
     */
    @objc optional func tableViewModel(_ tableViewModel: YYYBaseTableViewModel, sectionModel: YYYBaseTableViewSecionModel, viewForHeaderInSection section: Int) -> UIView?
    
    /**
     heightForHeaderInSection回调
     tableViewModel: 数据源TableViewModel
     sectionModel: 当前secion的sectionModel
     返回SectionHeader的高度
     */
    @objc optional func tableViewModel(_ tableViewModel: YYYBaseTableViewModel, sectionModel: YYYBaseTableViewSecionModel, heightForHeaderInSection section: Int) -> CGFloat
    
    
    /**
     点击内TableViewCell里按钮的回调
     cellModel: 点击Cell的CellModel
     */
    @objc optional func clickTableViewCellInsideButton(_ tableViewModel: YYYBaseTableViewModel, cellModel: YYYBaseTableViewCellModel, senderTag: Int)
    
    
    /**
     点击tableViewCell内CollectionViewCell回调
     collectionViewModel: 数据源collectionViewModel
     didSelectCellModel: 点击Cell的CellModel
     */
    @objc optional func clickInsideCollectionViewCell(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath)
    
}




class YYYBaseTableViewController: UITableViewController, YYYTableViewCellDelegate {
    
    /// 数据源viewModel
    var viewModel: YYYBaseTableViewModel? {
        didSet {
            // FIXME: - 业 注册重用
            // 注册cell,我也不知道这种方式好不好,待测试(或者可以像TableViewable一样给一个注册方式给VC)
            for sectionModel in viewModel!.sectionModels {
                
                for cellModel in sectionModel.cellModels {
                    
                    tableView.register(cellClassFromString(cellModel.cellClass).self, forCellReuseIdentifier: "\(cellModel.cellClass!)")
                }
            }
            tableView.reloadData()
        }
    }
    
    weak var delegate: YYYTableViewDelegate?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
    }
    
    
    private func setupTableView() {
        tableView.dataSource = self
        tableView.delegate = self
        tableView.backgroundColor = .background_light
        tableView.separatorStyle = .none
        tableView.estimatedRowHeight = 0
        tableView.sectionFooterHeight = 0
        tableView.sectionHeaderHeight = 0
        tableView.estimatedSectionHeaderHeight = 0
        tableView.estimatedSectionFooterHeight = 0
        tableView.showsVerticalScrollIndicator = false
        tableView.showsHorizontalScrollIndicator = false
        if #available(iOS 15.0, *) {
              tableView.sectionHeaderTopPadding = 0
        }
    }
    
    // MARK: - Private Method
    
    /// 字符串转类
    func cellClassFromString(_ className:String) -> AnyClass {
        // 1、获swift中的命名空间名
        var name = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String
        // 2、如果包名中有'-'横线这样的字符,在拿到包名后,还需要把包名的'-'转换成'_'下横线
        name = name?.replacingOccurrences(of: "-", with: "_")
        // 3、拼接命名空间和类名,”包名.类名“
        let fullClassName = name! + "." + className
        // 4、如果取不到,给个默认值
        let classType: AnyClass = NSClassFromString(fullClassName) ?? YYYTableViewCell.self
        // 本类type
        return classType
    }
    
    
    

    // MARK: - Table view data source

    override func numberOfSections(in tableView: UITableView) -> Int {
        let sections = viewModel?.sectionModels.count ?? 0
        return sections
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        let rows = viewModel?.sectionModels[section].cellModels.count ?? 0
        return rows
    }
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let height: CGFloat = self.delegate?.tableViewModel?(viewModel!, cellModel: viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row], heightForRowAt: indexPath) ?? 0
        if height != 0 {
            return height
        }
        
        return viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row].cellHeight
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let str = viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row].cellClass!
        // 得到AnyClass
        let anyClass: AnyClass = cellClassFromString(str)
        // 强转自己需要的类
        let classType = anyClass as! YYYTableViewCell.Type
        // 使用类方法 .cell来得到一个实例对象
        let cell = classType.cell(tableView)
        cell.cellModel = viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row]
        cell.cellDelagte = self
        return cell
    }
    
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.delegate?.tableViewModel?(viewModel!, didSelectCellModel: viewModel!.sectionModels[indexPath.section].cellModels[indexPath.row], didSelectRowAt: indexPath)
    }
    
    override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let haeder = self.delegate?.tableViewModel?(viewModel!, sectionModel: viewModel!.sectionModels[section], viewForHeaderInSection: section) ?? nil
        return haeder
    }
    
    override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        let height = self.delegate?.tableViewModel?(viewModel!, sectionModel: viewModel!.sectionModels[section], heightForHeaderInSection: section) ?? 0
        return height
    }
    
    
    // MARK: - YYYTableViewCell Delegate
    func clickInsideCollectionViewCell(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath) {
        self.delegate?.clickInsideCollectionViewCell?(collectionViewModel, didSelectCellModel: cellModel, didSelectItemAt: indexPath)
    }
    
    func clickInsideButton(_ cellModel: YYYBaseTableViewCellModel, senderTag: Int) {
        self.delegate?.clickTableViewCellInsideButton?(viewModel!, cellModel: cellModel, senderTag: senderTag)
    }
    
}

base TableViewModel 组件
YYYBaseTableViewModel

//  baseTableViewModel组件

import UIKit

class YYYBaseTableViewModel: NSObject {

    // inout修饰参数 传地址而不是值
    /// 构建函数参数闭包
    public typealias sectionModelsClosure = (inout [YYYBaseTableViewSecionModel]) -> Void
    
    // 数据源
    var sectionModels = [YYYBaseTableViewSecionModel]()
    
    init(sectionModelsClosure: sectionModelsClosure) {
        sectionModelsClosure(&sectionModels)
    }
    
}


class YYYBaseTableViewSecionModel: NSObject {

    /// 构建函数参数闭包
    public typealias cellModelsClosure = (inout [YYYBaseTableViewCellModel]) -> Void

    /// 数据源
    var cellModels = [YYYBaseTableViewCellModel]()
    
    init(cellModelsClosure: cellModelsClosure) {
        cellModelsClosure(&cellModels)
    }
    
}



class YYYBaseTableViewCellModel: NSObject {
    
    /// Cell类名
    var cellClass: String!
    /// Cell高度
    var cellHeight: CGFloat!
    /// Cell里的collectionViewModel
    var collectionViewModel: [AnyObject]!
    /// 标题
    var fieldTitle: String!
    /// 副标题
    var fieldSubTitle: String!
    /// 图片名
    var imageName: String!
    /// 图片URL
//    var imageURL: UIImage!
    /// 辅助字段
    var others: [String]!
    /// 是否只可读
    var isReadOnly: Bool!
    /// 辅助属性(存储数据源 - 回调用)
    var modelValue: AnyObject!
    
    override init() {
        super.init()
        
    }
    
}


// MARK: - 可扩展的 TabelCellModel

// 书本详情cellModel
class YYYBaseTableViewInfoCellModel: YYYBaseTableViewCellModel{
    
    
}

base TableViewCell 组件
YYYTableViewCell

//  基础TableViewCell

import UIKit

@objc protocol YYYTableViewCellDelegate: AnyObject {
    
    /**
     点击内CollectionViewCell回调
     collectionViewModel: 数据源collectionViewModel
     didSelectCellModel: 点击Cell的CellModel
     */
    @objc optional func clickInsideCollectionViewCell(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath)
    
    /**
     点击内TableViewCell里按钮的回调
     cellModel: 点击Cell的CellModel
     */
    @objc optional func clickInsideButton(_ cellModel: YYYBaseTableViewCellModel, senderTag: Int)
    
}

class YYYTableViewCell: UITableViewCell {
    
    /// 数据源cellModel
    var cellModel: YYYBaseTableViewCellModel! {
        didSet {
            setupUI()
            setCollectionCellModel()
        }
    }
    
    weak var cellDelagte: YYYTableViewCellDelegate?

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setup()
        
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    
    // MARK: - setup
    
    func setupUI() {
        // override
        
    }
    
    /// 构建内嵌CollectionView的cellModel
    func setCollectionCellModel() {
        // 因为这是个阅读类APP,所以会有大量的tableViewCell嵌套collectionViewCell,这里直接提供一个构建方法
        // override
        
    }
    
    class func cell(_ tableView: UITableView) -> YYYTableViewCell {
        return tableView.dequeueReusableCell(withIdentifier: "\(self)") as! YYYTableViewCell
    }
    

    // MARK: - Private Method
    
    /// 创建cell内 UIButton 通用方法
    func newButton() -> UIButton {
        let button = UIButton(type: .custom)
        button.addTarget(self, action: #selector(clickNewButton), for: .touchUpInside)
        addSubview(button)
        return button
    }
    
    @objc func clickNewButton(sender: UIButton) {
        self.cellDelagte?.clickInsideButton?(cellModel, senderTag: sender.tag)
    }
    
}

三、具体使用

1.tableViewController,table具体参数由业务决定:

lazy var tableViewController: YYYBaseTableViewController

2.YYYBaseTableViewModel,并传值给tableViewController.viewModel:

    // 构造tableViewModel
    func updateTableView() {
        
        let viewModel = YYYBaseTableViewModel { sectionModels in
            
            for data in dataSource {
                let sectionModel = YYYBaseTableViewSecionModel { cellModels in

                    if data.title.contains("Best") {
                        let cellModel1 = YYYBaseTableViewCellModel()
                        cellModel1.cellClass = "YYYThreeByThreeGridsStyle1TableCell"
                        cellModel1.fieldTitle = data.title
                        cellModel1.cellHeight = scrW(623.5)
                        cellModel1.imageName = "hometop_bestbg"
                        cellModel1.collectionViewModel = data.bookList
                        cellModels.append(cellModel1)
                    }
                    else {
                        let cellModel1 = YYYBaseTableViewCellModel()
                        cellModel1.cellClass = "YYYOneRowSlidableStyle1TableCell"
                        cellModel1.cellHeight = scrW(190)
                        cellModel1.collectionViewModel = data.bookList
                        cellModels.append(cellModel1)
                    }
                }
                sectionModels.append(sectionModel)
            }
        }
        tableViewController.viewModel = viewModel
    }

3.实现业务需要的一些通用delegate

    // MARK: - YYYTableViewDelegate
    
    // 点击里侧嵌套CollectionCell的通用回调,有需要把YYYBaseCollectionViewCellModel替换成自己自定义XXXCellModel的就好了
    func clickInsideCollectionViewCell(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath) {
        // 书籍cell样式1
        if cellModel.cellClass == "YYYBookCoverStyle1CollectionViewCell" {
            
            // 点击跳转 书籍详情页
            let bookInfo = BookInfoViewController()
            // 构造函数的 modelValue: AnyObject来存储数据源属性,到回调里强转使用。
            let book = cellModel.modelValue as? HomeTop ?? HomeTop()
            bookInfo.bookID = book.topId
            jumpvc(viewController: bookInfo)
        }
        
    }
    // 测试cell中按钮点击情况
    func clickTableViewCellInsideButton(_ tableViewModel: YYYBaseTableViewModel, cellModel: YYYBaseTableViewCellModel, senderTag: Int) {
        print("AKAK")
    }

四、优点和缺点

优点一、代码规范

规范常用代码写法,大部份业务都是相似的代码结构,接手后维护成本更低,也能避免不同水平程序员写出来不同风格的代码,可阅读性更好

优点二、内聚性

使构建常见的table时,代码更加内聚,构建新代码时间成本更低,维护旧代码可一目了然。

优点三、统一cellModel属性,业务model不会渗透通用cell和controller代码,cellModel在回调中拿来即用

构建常见的table时,后台无论怎么变化返回的数据结构,共用tableCell都是用同一个通用的cellModel属性,不会造成引入业务model而渗透cell和controller回调的代码。同时写新UI样式时更方便,属性更统一,开发相似样式cell的时候也更容易做出更改。

缺点一、增加学习成本

熟悉框架(引用,构建ViewModel)时,因为和平常大家熟知的(继承,实现代理)写代码方式不一样,所以会有一定的学习成本。

缺点二、无法应对复杂场景的UI

比如无法用 estimatedRowHeight约束布局 实现动态布局列表,框架多用于快速构建实现常见的静态布局。当然,写复杂场景时就不需要引用 lazy var tableViewController: YYYBaseTableViewController 了。如果APP需要初始化一些通用配置, 搭建一个BaseViewController 进行继承即可。

最最最后,完结撒花

告辞.jpeg
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容