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
swift 代码解读复制代码// 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
kotlin 代码解读复制代码// baseTableViewModel组件
import UIKit
class YYYBaseTableViewModel: NSObject {
// inout修饰参数 传地址而不是值
/// 构建函数参数闭包
public typealias sectionModelsClosure = (inout [YYYBaseTableViewSecionModel]) -> Void
// 数据源
var sectionModels = [YYYBaseTableViewSecionModel]()
init(sectionModelsClosure: sectionModelsClosure) {
sectionModelsClosure(§ionModels)
}
}
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
swift 代码解读复制代码// 基础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具体参数由业务决定:
swift 代码解读复制代码lazy var tableViewController: YYYBaseTableViewController
2.YYYBaseTableViewModel,并传值给tableViewController.viewModel:
ini 代码解读复制代码 // 构造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
swift 代码解读复制代码 // 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
进行继承即可。
评论记录:
回复评论: