笔者文笔有限,写的不是很清楚,使用中遇到问题可私信或加入cocoapod-sled交流群提出疑问
TableViewSections 目标是简化复杂列表的开发,让代码结构更清晰、易用维护,提高多人协作效率,同时提高模块(Secton)级复用性,当有足够多的通用模块时可以支持页面一定的动态性。
TableViewSections 非常简单的框架,很容易上手,学习成本低
模块: 这里指 tableView 的 indexPath.section 中所有 cell 集合
前言
当开发一个相对复杂的列表页面,如何实现呢?
可能要考虑以下的问题:假设决定使用UITableView
- 分工问题:如果工期比较紧张,可能要多人合作才能完成
- 模块如何划分?页面框架和单独模块的边界怎么清晰界定?
- 单个模块代码怎么调试?
- 合并代码时可能遇到的冲突?
- 基于
UITableView的实现方案
- 设置
tabView.dataSource和tabView.delegate需要一个DataSource(ViewModel 或 ViewController)
- 处理页面大部分逻辑,代码量大?迭代维护困难?
- 区分不同的 Section 和 Cell
使用枚举、二维数组或其他方式
- 数据源动态更新如何处理?
- 迭代维护是否方便?
- 网络请求和数据缓存如何处理
每个模块的请求接口独立(有一个或多个接口) 整个页面可能还有一个框架接口定义页面可用模块及展示顺序(有一定的动态性)
- 模块复用时,接口能否快速复用?
- 可交互的模块如何处理:比如某个模块可关闭,关闭后不再展示
- 代码复用问题
- 如果一个模块及其逻辑在多个地方使用,怎么能够快速复用,避免代码拷贝
- 迭代维护成本
- 对模块的增、删、改能否快速实现,减少无关代码干扰,缩小修改影响范围
- 修改、增加模块,是否需要修改多个地方?
- 删除模块,能否快速删除,不遗留大量冗余代码?
- 跨组件复用
- 工程组件化粒度较小时,代码能否方便的跨组件复用?
下面向大家推荐我的实现方案 TableViewSections
Github地址:github.com/git17997950…
TableViewSections
采用分层解耦的思想,可以很好的解决上面提到的问题
- 降低代码复杂度,增加代码的可维护性
- 提高代码聚合度,增加代码的可复用性
- 通过组合的方式,增加代码的可扩展性,提高多人协作的编码体验和效率
如何分层?
强化 Section,给他创建一个实体,承载更多的逻辑。
抽象出 SectionType 类型负责管理自己的数据处理、视图状态、用户交互、业务逻辑等。DataSource 那一层只需要负责 Section 的组装和页面框架相关的逻辑。
从:TableView -> DataSource -> Cells
变为:TableView -> DataSource -> Sections -> Cells
框架类型介绍
BaseTableViewModel
作为UITableView的 DataSource 和 Delegate,实现了部分代理方法,并转发到 Section。
简单页面可直接使用,真以为 TableView 和 Section 的粘合剂。复杂页面可以子类化,按需实现更多的代理方法,记得转发到 Section。
还提供了一些其他特性:
- Number of rows cache:避免数据源瞬时变更引起的崩溃
- 便利的刷新和删除方法,谨慎使用
TableViewModelReloadable.swift - 上下文,方便的获取
tableViewviewController和自定义附加信息
TableViewModelReloadable.swift
对BaseTableViewModel的扩展,提供一些便利的刷新方法
EstimatedTableViewModel
BaseTableViewModel 的子类,实现并转发了以下方法
func tableView(UITableView, estimatedHeightForRowAt: IndexPath) -> CGFloat
TableViewSectionType
一个 protocol,对 Section 的抽象,负责处理 BaseTableViewModel 转发的代理方法,并处理 Section 自己的业务逻辑。
TableViewSectionsContext
上下文,可在 Section 内方便的获取 tableView viewController 和自定义附加信息
实践
介绍完了 TableViewSections 的设计思想和实现方案,下面我们看看如何使用
定义一个Section
Cell自动注册:juejin.cn/post/719500…
// 定义一个组合类型,SectionLoaderType 实现请查看补充
typealias PlanTableViewSectionType = TableViewSectionType & SectionLoaderType
// 定义一个 Section 实体类
class PlanNoticeSection: PlanTableViewSectionType {
// 业务 - 组合类型
let type: PlanType
// 业务 - 数据模型
var model: PlanNoticeModel?
// 框架 - 上下文,需要获取上下文,就定义下面这个存储属性,框架内部自动赋值
var context: TableViewSectionsContext?
// 框架 - 控制Section是否显示
var section_isDisplay: Bool = false
init(type: PlanType) { self.type = type }
// 使用Cell自动注册逻辑,可不用实现这个方法
// https://juejin.cn/post/7195005912653856827
// static func register(for tableView: UITableView) {
// // 注册用到的Cell,外部需要调用这个方法进行注册
// }
// SectionLoaderType 声明的方法,按需加载缓存
func loadCache() {
// 按需加载缓存数据
}
// SectionLoaderType 声明的方法,网络请求
func loadData(callback: @escaping ErrorTask) {
PlanAPI.otherApi(params: ["type": type.rawValue]).reqeust { [weak self] in
defer { callback(nil) }
guard let self = self else { return }
self.section_isDisplay = true
self.model = PlanNoticeModel(demoImageNamed: "plan_(self.type.rawValue)_notice")
}
}
// 实现 BaseTableViewModel 转发的代理方法
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model == nil ? 0 : 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Cell 自动注册 https://juejin.cn/post/7195005912653856827
let cell = tableView.ns.dequeueCell(PlanNoticeCell.self, for: indexPath)
model.run(cell.update(with:))
return cell
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat? {
return 768
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// TODO: 跳转
let vc = DemoDetailViewController()
vc.navigationItem.title = "通知详情"
context?.viewController?.navigationController?.pushViewController(vc, animated: true)
}
}
定义 BaseTableViewModel 子类(按需)
// Demo使用自适应高度,所以继承自 EstimatedTableViewModel
class PlanType1TableVM: EstimatedTableViewModel {
// 网络请求逻辑
func loadData(callback: @escaping ErrorTask) {
// 请求页面框架
PlanAPI.someApi.reqeust { [weak self] in
// 可以根据框架数据动态组装section
let sections: [PlanTableViewSectionType] = [
ClosableSection(),
PlanType1HeaderSection(),
PlanType1IntroduceSection(),
PlanNoticeSection(type: .type1), // section 复用
PlanType1GrowthSection(),
PlanProfitRatioSection(type: .type1), // section 复用
PlanType1NewPlanSection(),
PlanHoldingSection(type: .type1), // section 复用
PlanManagerSection(type: .type1), // section 复用
PlanQASection(type: .type1), // section 复用
PlanTopCommentsSection(type: .type1), // section 复用
]
// 请求 sction 的数据
self?.loadSections(sections, callBack: callback)
}
}
// Demo采用所有Section数据加载完毕在渲染的策略,也可以分开渲染
// - 使用 section_isDisplay 控制 Section 是不展示: sections(数据源) -> displayedSections(实际展示的,section_isDisplay == true)
// - 前几个 Section 使用占位或缓存,提高视觉渲染速度
private func loadSections(_ sections: [PlanTableViewSectionType], callBack: @escaping ErrorTask) {
let group = DispatchGroup()
var err: Error?
sections.forEach { (sec) in
group.enter()
sec.loadData { (error) in
group.leave()
if error != nil { err = error }
}
}
group.notify(queue: .main) { [weak self] in
self?.updateSections(sections)
self?.reloadTableView()
callBack(err)
}
}
}
定义ViewController和TableView
class PlanType1ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
// 页面状态 - 加载中
tableView.ps.item = PSLabelItem.empty(text: "加载中...")
.config { item in
item.layoutOffset = CGPoint(x: 0, y: -150)
}
viewModel.loadData { [weak tableView] error in
// 页面状态 - 移除
tableView?.ps.item = nil
}
}
let viewModel = PlanType1TableVM()
lazy var tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .grouped)
// viewModel 关联 tableView,内部把自己设置为 tableView 的 DataSource 和 Delegate
viewModel.associateTableView(tableView, viewController: self, additional: nil)
tableView.tableHeaderView = UIView()
tableView.tableFooterView = UIView()
tableView.separatorStyle = .none
tableView.sectionHeaderHeight = 12
tableView.sectionFooterHeight = .leastNonzeroMagnitude
// 这里应该注册用到的Cell,使用Cell自动注册可以省略
// https://juejin.cn/post/7195005912653856827
// PlanNoticeSection.register(for: tableView)
return tableView
}()
}
小结
这样我们就使用 TableViewSections 完成了列表的创建,更多实现细节可查看Demo。
从上面的示例代码可以看到,代码编写习惯和直接使用 UITableView 基本没有区别,只是中间多了一层 SectonType,把业务逻辑做了分割和聚合。
笔者精力有限,只提供了简单的使用示例,TableViewSections 是一个简单且扩展性比较强的框架,更多使用方式等待你的发掘。
更多使用场景
分页加载
有的页面最底部的模块可以分页无限加载,TableViewSectionType 可以相关的逻辑,不过要记得把分页的 Section 放到最底部
模块动态显示隐藏
- 可关闭的模块,关闭后不再展示
- 请求到数据后才能确定是否展示
- 根据条件动态的显示和隐藏
// 可关闭的模块,关闭后不再展示
class ClosableSection: PlanTableViewSectionType {
// 控制 Section 是否展示
var section_isDisplay: Bool = true
// 定义为存储属性,框架内部自动设置
var context: TableViewSectionsContext?
// 省略部分代码 ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.ns.dequeueCell(ClosableCell.self.self, for: indexPath)
// 点击关闭按钮
// section_isDisplay 赋值维 false
// 调用 `reloadTableViewAndDisplayedSections` 刷新数据源
cell.onTapCloseButtonClosure = { [weak self] in
guard let self = self else { return }
self.section_isDisplay = false
self.context?.viewModel?.reloadTableViewAndDisplayedSections()
}
return cell
}
// 省略部分代码 ...
}
补充
SectionLoaderType
typealias ErrorTask = OneParamTask<Error?>
protocol SectionLoaderType {
func loadCache()
func loadData(callback: @escaping ErrorTask)
}
extension SectionLoaderType {
func loadCache() { }
func loadData(callback: @escaping ErrorTask) {
callback(nil)
}
}
其它
推荐一个Cocoapods组件二进制化插件 cocoapods-sled,即插即用,快速提高编译效率。