如何为UITableView添加多个浮动子分区以展示树形层级?
我来给你梳理一套完整的落地方案,刚好之前做过类似的多级树形Section需求,结合你已经调研的几个方向,咱们把细节补全:
核心思路总结
因为原生plain样式的UITableView只支持单层级悬浮Section,所以我们的核心方案是:将树形层级的每个父节点标记为「伪分区行」,配合导航栏下方的自定义固定头部,通过监听滚动事件联动更新头部内容,模拟多级悬浮Section的效果。
具体实现步骤
1. 树形节点数据结构设计
首先定义一个包含层级标记、伪分区标识的节点模型,方便后续扁平化处理和UI展示:
class TreeNode { let level: Int // 0=Building, 1=Zone, 2=Floor...以此类推 let title: String var children: [TreeNode] = [] var isPseudoSection: Bool // 标记是否为伪分区行 var isExpanded: Bool = true // 控制子节点展开/折叠状态 init(level: Int, title: String, isPseudoSection: Bool = false) { self.level = level self.title = title self.isPseudoSection = isPseudoSection } }
比如Building节点设置level=0且isPseudoSection=true,Zone节点设置level=1且isPseudoSection=true,以此类推。
2. 树形结构转扁平数据源
把嵌套的树形结构转成一维数组,作为UITableView的数据源,同时支持展开/折叠逻辑:
private var flattenNodes: [TreeNode] = [] private let rootNode: TreeNode // 你的根节点(比如所有Building的父节点) // 递归生成扁平数组 private func generateFlattenNodes(from node: TreeNode) -> [TreeNode] { var result: [TreeNode] = [] // 先加入当前伪分区节点 if node.isPseudoSection { result.append(node) } // 如果节点展开,加入所有子节点(递归处理) if node.isExpanded { for child in node.children { result.append(contentsOf: generateFlattenNodes(from: child)) } } return result } // 在viewDidLoad或数据更新时调用 flattenNodes = generateFlattenNodes(from: rootNode) tableView.reloadData()
3. UITableView单元格配置
根据节点的层级和伪分区属性,配置不同的UI样式:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return flattenNodes.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let node = flattenNodes[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: "TreeCell", for: indexPath) // 伪分区行样式区分(比如加粗字体、背景色) if node.isPseudoSection { cell.textLabel?.font = .boldSystemFont(ofSize: 16) cell.backgroundColor = .systemGray6 // 可以加展开/折叠箭头 cell.accessoryType = node.isExpanded ? .disclosureIndicator : .none } else { cell.textLabel?.font = .systemFont(ofSize: 15) cell.backgroundColor = .systemBackground cell.accessoryType = .none } // 根据层级设置缩进(或自定义leading约束更灵活) cell.indentationLevel = node.level cell.indentationWidth = 20 cell.textLabel?.text = node.title return cell }
4. 自定义固定头部视图(导航栏下方)
创建一个固定在导航栏下方的头部,用来展示当前层级的伪分区标题:
private let sectionHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 44)) private let headerTitleLabel = UILabel() override func viewDidLoad() { super.viewDidLoad() // 配置头部视图 sectionHeaderView.backgroundColor = .systemGray6 headerTitleLabel.font = .boldSystemFont(ofSize: 16) headerTitleLabel.textColor = .label headerTitleLabel.frame = sectionHeaderView.bounds sectionHeaderView.addSubview(headerTitleLabel) // 添加到视图层级,确保在tableView上方 view.addSubview(sectionHeaderView) view.bringSubviewToFront(sectionHeaderView) // 给tableView设置顶部内边距,避免内容被头部遮挡 tableView.contentInset = UIEdgeInsets(top: 44, left: 0, bottom: 0, right: 0) // 初始化头部标题 updateHeaderTitle(with: flattenNodes.first(where: { $0.isPseudoSection })) } private func updateHeaderTitle(with node: TreeNode?) { headerTitleLabel.text = node?.title ?? "" }
5. 滚动联动更新头部
通过scrollViewDidScroll监听滚动,实时更新头部显示的伪分区标题,模拟悬浮效果:
func scrollViewDidScroll(_ scrollView: UIScrollView) { guard let visibleRows = tableView.indexPathsForVisibleRows else { return } // 获取最顶部的可见行 let topRowIndex = visibleRows.min { $0.row < $1.row }?.row ?? 0 let currentNode = flattenNodes[topRowIndex] var targetNode: TreeNode? // 如果当前行是伪分区,直接用它的标题 if currentNode.isPseudoSection { targetNode = currentNode } else { // 往前找最近的已展开伪分区 for i in (0...topRowIndex).reversed() { if flattenNodes[i].isPseudoSection && flattenNodes[i].isExpanded { targetNode = flattenNodes[i] break } } // 检查下一个伪分区是否即将滚动到头部位置,准备切换标题 for i in topRowIndex..<flattenNodes.count { let node = flattenNodes[i] guard node.isPseudoSection else { continue } let cellRect = tableView.rectForRow(at: IndexPath(row: i, section: 0)) let cellTopY = cellRect.origin.y - scrollView.contentOffset.y // 当伪分区cell顶部触碰到头部底部时,切换标题 if cellTopY <= sectionHeaderView.frame.height { targetNode = node break } } } updateHeaderTitle(with: targetNode) }
6. 伪分区行的展开/折叠交互
处理伪分区行的点击事件,实现子节点的展开和折叠:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let node = flattenNodes[indexPath.row] guard node.isPseudoSection else { return } // 切换展开状态 node.isExpanded.toggle() // 重新生成扁平数组并刷新 flattenNodes = generateFlattenNodes(from: rootNode) tableView.reloadData() // 滚动回当前伪分区位置,保持用户视野 if let newIndex = flattenNodes.firstIndex(where: { $0 === node }) { tableView.scrollToRow(at: IndexPath(row: newIndex, section: 0), at: .top, animated: true) } }
关键注意事项
- 缩进优化:如果原生
indentationLevel满足不了需求,可以自定义Cell,通过约束动态调整内容的leading距离,适配不同层级。 - 性能优化:如果树形结构非常庞大,可以缓存扁平化后的数组,只在节点展开/折叠时局部更新,避免全量刷新。
- 适配安全区:如果页面有刘海或底部安全区,要调整头部视图的frame和tableView的contentInset,避免布局错位。
内容的提问来源于stack exchange,提问作者Sebastian Dwornik




