SLExpandableTableView源码解析(上):numberOfRowsInSection如何把一个分区的多行折叠成1行
【免费下载链接】SLExpandableTableViewSLExpandableTableView is a UITableView subclass that gives you easy access to expandable and collapsable sections by just implementing a few more delegate and dataSource protocols. (iPhone, iPad, iOS)项目地址: https://gitcode.com/gh_mirrors/sl/SLExpandableTableView
SLExpandableTableView 是一款专为 iOS 打造的开源组件,它是 UITableView 的子类,只需实现几个额外的 DataSource / Delegate 方法,就能让列表的分区(Section)获得"一键展开 / 折叠"的手风琴效果,同时支持 iPhone 与 iPad。本文聚焦源码中一个最巧妙的技巧:库如何利用numberOfRowsInSection这个回调,把分区里的多行数据"折叠"成 1 行。
🎯 一、这个组件解决了什么问题
原生 UITableView 并不会自动提供分区折叠能力——想让某个 Section 收起来,你得自己挪动数据、重算行数,代码又长又容易出动画 bug。SLExpandableTableView 的思路是:把行数的"记账权"抢过来。它成为 tableView 真正的 dataSource,对外再暴露一层自己的协议,由它决定"每个分区现在显示几行"。
效果如下图所示:黄色单元格是数据行,白色带箭头的行就是折叠/展开的"开关行":
对使用方来说,只需额外实现 3 个数据源方法(定义在 SLExpandableTableView/SLExpandableTableView.h):
canExpandSection:—— 这个分区是否可展开needsToDownloadDataForExpandableSection:—— 展开前是否需要先拉数据expandingCellForSection:—— 返回那个"开关行"单元格的样式
🔑 二、关键前提:第 0 行被"征用"了
看懂折叠原理前,先要知道库的一个约定:在可展开的分区里,row 0 永远不是你的数据行,而是那个展开/折叠开关行。你的数据从 row 1 开始排。
看示例工程 Tests/SLExpandableTableViewTests/SLExpandableTableViewController.m 中的实现就很清楚:一个分区有 4 条数据,但返回的行数是dataArray.count + 1,多出来的那一行就是给开关行"留位置"。
相应地,库重写了cellForRowAtIndexPath:(SLExpandableTableView/SLExpandableTableView.m 第 436-460 行):当indexPath.row == 0且该分区可展开时,它不再询问你的数据源,而是直接返回你通过expandingCellForSection:提供的开关行,并根据当前状态把它摆成"已展开"或"已折叠"的样式。
🧠 三、核心拆解:numberOfRowsInSection 的折叠魔法
真正的魔术发生在库自己实现的这个回调里(SLExpandableTableView/SLExpandableTableView.m 第 415-434 行),逻辑浓缩后就这几行:
if ([self.myDataSource tableView:self canExpandSection:section]) { if ([self.myDataSource tableView:tableView numberOfRowsInSection:section] == 0) { return 0; // ① 空分区直接返回 0 } self.expandableSectionsDictionary[key] = @YES; if ([self.showingSectionsDictionary[key] boolValue]) { return [self.myDataSource tableView:tableView numberOfRowsInSection:section]; // ② 已展开:返回真实行数 } else { return 1; // ③ 未展开:只返回 1 行 } }三步拆解:
- 先问你的数据源:这个分区能不能展开?不能就原样透传真实行数,完全不干预。
- 能展开则登记状态:把该分区记入
expandableSectionsDictionary,供点击回调和单元格渲染时查询。 - 按状态"谎报"行数:如果该分区当前没有展开(
showingSectionsDictionary为 NO),就向 UITableView 报告"这个分区只有 1 行"——于是屏幕上只剩下开关行,其余多行被整体"藏"了起来。
这就是标题的答案:折叠不是隐藏单元格,而是让 UITableView 根本不知道那些行的存在。展开时把行数改回真实值,配合插入行动画,视觉上的"展开"就完成了。
为了支撑这套记账,库内部维护了 4 个状态字典(SLExpandableTableView/SLExpandableTableView.h 第 63-66 行):
| 字典 | 作用 |
|---|---|
expandableSectionsDictionary | 记录哪些分区可展开(在 numberOfRowsInSection 回调中顺手写入) |
showingSectionsDictionary | 记录分区当前是"展开"还是"折叠",是折叠判断的依据 |
downloadingSectionsDictionary | 记录哪些分区正在等待数据下载,用来在开关行上显示 loading |
animatingSectionsDictionary | 记录哪些分区正在播放动画,用于区分普通的 willDisplayCell 回调 |
注意一个小细节:reloadData时库会清空这些状态(reloadDataAndResetExpansionStates:,第 337-339、150-154 行),全部回到折叠态——因为旧索引对应的状态已经不可信了。
🎬 四、点击后的展开与折叠动画(简析)
折叠成 1 行只是"静态"的一半,另一半是用户点击开关行后的动画处理(didSelectRowAtIndexPath:,第 385-411 行):
- 点中的是可展开分区的 row 0:已展开则调
collapseSection:,未展开则调expandSection:; - 点中的是数据行(row > 0):原样转发给你自己的 delegate,业务逻辑不受影响。
collapseSection:(第 268-328 行)的动画做法:在beginUpdates/endUpdates事务里,把 row 1 到 row N-1 一次性deleteRowsAtIndexPaths:删掉,同时把开关行切回"折叠"样式;expandSection:(第 201-266 行)则是反过来insertRowsAtIndexPaths:。默认动画为UITableViewRowAnimationFade(reloadAnimation,第 134 行)。
还有一个性能保险丝:maximumRowCountToStillUseAnimationWhileExpanding(默认NSIntegerMax)。当展开行数超过阈值时,库会放弃逐行动画,直接整体reloadData,避免大分区卡顿。
⚠️ 五、新手必看的 5 个坑
- 数据源行数必须 +1:可展开分区里要给开关行留位置,否则展开后的行数对不上,动画会崩。
- row 0 没有选择回调:头文件(第 78 行)明确说明,可展开分区的 row 0 不会再触发
didSelectRowAtIndexPath:回调,不要在上面挂跳转逻辑。 - 别直接读
tableView.dataSourcegetter:它返回的是库自己,要访问自己的数据源请用myDataSource(头文件第 82-84 行有专门注释)。 - 懒加载要闭环:声明需要下载数据后,成功后调用
expandSection:animated:,失败调用cancelDownloadInSection:,否则开关行会一直停在 loading 态。 - reloadData 会重置折叠状态:刷新数据后所有分区回到折叠态,且"可展开"标记会被重新询问,属于预期行为。
📁 六、核心文件速查
| 文件 | 说明 |
|---|---|
SLExpandableTableView/SLExpandableTableView.h | 对外接口:3 个数据源协议方法 + 展开/折叠公开方法 |
SLExpandableTableView/SLExpandableTableView.m | 核心实现:行数折叠、点击分发、展开/折叠动画 |
Tests/SLExpandableTableViewTests/SLExpandableTableViewController.m | 完整示例:含懒加载下载的分区列表演示 |
Tests/Podfile | 通过 CocoaPods 引入的依赖示例 |
SLExpandableTableView.podspec | 组件的 CocoaPods 发布配置 |
到这里,折叠"藏行"的原理已经讲透;展开动画的完整时序、下载态与状态字典的交互细节,留待源码解析(下)继续。
【免费下载链接】SLExpandableTableViewSLExpandableTableView is a UITableView subclass that gives you easy access to expandable and collapsable sections by just implementing a few more delegate and dataSource protocols. (iPhone, iPad, iOS)项目地址: https://gitcode.com/gh_mirrors/sl/SLExpandableTableView
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考