我用一个 UITableView,干掉了 80% 复杂页面
一.引言在 iOS 开发中“复杂页面”几乎是所有人都会遇到的问题。比如一个典型的专家详情页通常包含用户信息数据统计胜率、收益图表推荐内容历史列表这个页面中倒是包含了列表所以大家会考虑到UITableView有些时候可能并不包含复用列表这时候大家就很难想到UITableView。很多人的写法是这样的UIScrollView 多个子 View各种 if/else 控制显示隐藏需求一改牵一发动全身写到后期往往会变成一个“谁都不敢动”的页面。二. 问题的本质采用了不合理的方式描述页面出现这种现象的原因主要是因为大多数人对页面的理解是这样的页面 一堆 View 的组合。但实际上这类页面更接近页面 多个“区块Section”的组合。一旦我们换成这个角度来理解页面那么实现的方式将会完全不同。三. 核心思路把页面构建成Section 列表我在项目中采用了一种非常简单但有效的方式首先我的项目中会有两个类在通用组件内我们以其中一个PHBaseSectionItem为例import UIKit open class PHBaseSectionItem: NSObject { /// cell类 public var tabCellClass: UITableViewCell.Type? /// cell类 public var collectionCellClass: UICollectionViewCell.Type? /// cell标识 public var cellIdentifier: String /// 组头标识 public var headerIdentifier: String /// 组标题 public var title: String /// 子列表数据 public var subItems: [Any] [] /// 子列表size public var itemSize: CGSize CGSize.zero /// 组头高度 public var headerHeight: CGFloat 0 /// 组尾高度 public var footerHeight: CGFloat 0 }我的这个类可以用在UITableView列表中也同样可以使用在UICollectionView列表中我们就以UITableView为例tabCellClassUITableViewCell.Type UITableViewCell的类型。cellIdentifier字符串该组UITableViewCell的标识。headerIdentifier字符串该组组头的标识。title字符串该组的标题。subItemsAny类型数组如果页面是多类型的列表会用到这个。headerHeightCGFloat该组组头高度。footerHeightCGFloat该组组尾高度。然后在页面内创建一个页面结构的数据列表/// 页面结构 private var dataList: [PHBaseSectionItem] []这些东西出来之后页面将不再由View决定而是由一组PHBaseSectionItem决定。页面结构示意图我们把示例简化一点就不加图表了哈你可以把整个页面理解为用户信息统计数据最新方案列表历史方案列表四.关键设计引用 Builder 统一构建页面结构然后我们引入一个Builder它主要用来根据我们需要显示或者隐藏的内容来构建列表数据 dataListclass PHContributorDetailBuilder: NSObject { /// 构建专家详情列表数据 static func buildDataList()-[PHSectionItem] { var list:[PHSectionItem] [] // 信息 let infoSectionItem PHSectionItem() infoSectionItem.cellIdentifier PHContributorInfoCell infoSectionItem.tableViewCellClass PHContributorInfoCell.self list.append(infoSectionItem) // 近期战绩 let lastRecordSectionItem PHSectionItem() lastRecordSectionItem.cellIdentifier PHContributorRecordCell lastRecordSectionItem.tableViewCellClass PHContributorRecordCell.self list.append(lastRecordSectionItem) // 在售方案 let saleSectionItem PHSectionItem() saleSectionItem.cellIdentifier PHSchemeDetailOtherCell saleSectionItem.tableViewCellClass PHSchemeDetailOtherCell.self saleSectionItem.headerIdentifier PHContributorSectionHeaderView saleSectionItem.headerHeight 44 saleSectionItem.title Latest Insights list.append(saleSectionItem) // 历史方案 let historySectionItem PHSectionItem() historySectionItem.cellIdentifier PHSchemeDetailOtherCell historySectionItem.tableViewCellClass PHSchemeDetailOtherCell.self historySectionItem.headerIdentifier PHContributorSectionHeaderView historySectionItem.headerHeight 44 historySectionItem.title Past Insights list.append(historySectionItem) return list } }Builder 负责定义页面有哪些区块每个区块用什么 CellHeader 长什么样Header高度等等。五.tableView 的角色不是核心而是“渲染器”在这套架构下UITableView 的职责被极度简化private let tableView UITableView(frame: .zero, style: .plain)它只做三件事注册 Cell根据 dataList渲染 Cell根据 dataList刷新 UIreloadDataUITableView 不再是“列表控件”而是一个“页面渲染引擎”。六.如何处理“特殊区块”在我们的这个页面中有两个列表属于不同于其它类型的但是又不多这种情况我们就直接在视图控制器内通过sectionItem的标识或者标题来单独处理这两组数据了。例如我的判断是if sectionItem.title 最新方案 { return schemeList.count } if sectionItem.title 历史方案 { return historySchemeList.count }而如果确实有很多不同的列表大家也看见了当前PHBaseSectionItem是Open修饰的也就是我们具备最大的权限完全可以继承自PHBaseSectionItem创建一个比较个性的数据结构每个结构中都有可以变化的数据其实在PHBaseSectionItem也提供了自列表的入口大家也可以直接使用只是因为是Any类型需要进行的判断会多一些。整体策略还是统一骨架 局部特判七. 这套方案带来的工程价值1. 页面不会越改越乱:结构由 Builder 控制而不是 VC。避免 if/else 爆炸2. 新增模块成本极低:新增一个区块只需要增加一个 SectionItem不需要改动页面整体结构。3. 天然支持“动态页面”比如AB Test不同用户展示不同模块只需要改 Builder。4. ViewController 复杂度被锁死VC 只负责渲染绑定数据响应事件不会膨胀成 1000 行的“上帝类”八. 结语我们并没有在“优化 UITableView 的写法” 而是在重新定义“页面应该如何被描述”。过去我们用 View 拼页面现在我们用 Section 描述页面。当页面结构被抽象成数据之后- UITableView 只是一个渲染工具- ViewController 不再承载结构复杂度- 页面也不会随着需求迭代而失控UITableView 没变 但我们看待它的方式变了。而这才是这套方案真正的价值。