You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何为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=0isPseudoSection=true,Zone节点设置level=1isPseudoSection=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

火山引擎 最新活动