You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现带树形结构子单元格的UITableView展开/折叠功能?

树形UITableView展开折叠解决方案

你的核心问题在于直接修改原始树形数据源+用行号跟踪展开状态,这种方式不仅无法处理嵌套子节点的折叠,还极易因行号计算错误导致表格崩溃。下面是更可靠的优化方案:

核心思路

  1. 保留原始树形数据不变,作为真实数据来源
  2. 用节点唯一ID跟踪展开状态(而非行号,行号会随展开折叠动态变化)
  3. 动态生成扁平化展示数据源,根据展开状态递归遍历原始树生成
  4. 可选使用Diffable数据源实现流畅动画,避免手动操作行的崩溃风险

完整代码实现

1. 补全数据模型(假设你的MyObject结构)

struct MyObject {
    let name: String
    let id: String
    let parentId: String?
    var children: [MyObject]?
}

// 让模型遵循Hashable,用于Diffable数据源
extension MyObject: Hashable {
    static func == (lhs: MyObject, rhs: MyObject) -> Bool {
        lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

2. 定义核心属性

// 原始树形数据,始终保持不变
var objectTree: [MyObject] = [
    MyObject(name: "Parent 1", id: "1", parentId: nil, children: [
        MyObject(name: "Child 1", id: "2", parentId: "1"),
        MyObject(name: "Child 2", id: "3", parentId: "1")
    ]),
    MyObject(name: "Parent 2", id: "4", parentId: nil, children: [
        MyObject(name: "Child 3", id: "5", parentId: "4", children: [
            MyObject(name: "Grandchild 1", id: "6", parentId: "5")
        ])
    ])
]

// 记录节点展开状态,key为节点唯一ID
var expandedNodeIds = Set<String>()

// 动态生成扁平化展示数据源
private var displayDataSource: [MyObject] {
    generateDisplayList(from: objectTree)
}

3. 递归生成展示数据源

private func generateDisplayList(from nodes: [MyObject]) -> [MyObject] {
    var result = [MyObject]()
    for node in nodes {
        result.append(node)
        // 如果节点有子节点且处于展开状态,递归添加所有子节点
        if let children = node.children, expandedNodeIds.contains(node.id) {
            result.append(contentsOf: generateDisplayList(from: children))
        }
    }
    return result
}

4. 配置UITableView数据源(可选Diffable版本)

// 定义Section类型
enum TreeSection { case main }

// 初始化Diffable数据源
private var dataSource: UITableViewDiffableDataSource<TreeSection, MyObject>!

override func viewDidLoad() {
    super.viewDidLoad()
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "TreeCell")
    
    // 配置Diffable数据源
    dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, node in
        let cell = tableView.dequeueReusableCell(withIdentifier: "TreeCell", for: indexPath)
        cell.textLabel?.text = node.name
        // 设置缩进层级,体现树形结构
        cell.indentationLevel = self.getIndentLevel(for: node)
        cell.indentationWidth = 24
        return cell
    }
    
    // 初始加载数据
    updateTableView()
}

// 更新表格数据
private func updateTableView() {
    var snapshot = NSDiffableDataSourceSnapshot<TreeSection, MyObject>()
    snapshot.appendSections([.main])
    snapshot.appendItems(displayDataSource)
    dataSource.apply(snapshot, animatingDifferences: true)
}

// 计算节点的缩进层级
private func getIndentLevel(for node: MyObject) -> Int {
    var level = 0
    var currentParentId = node.parentId
    while let parentId = currentParentId {
        level += 1
        currentParentId = findNodeById(parentId)?.parentId
    }
    return level
}

// 根据ID查找节点的辅助方法
private func findNodeById(_ id: String) -> MyObject? {
    func search(in nodes: [MyObject]) -> MyObject? {
        for node in nodes {
            if node.id == id { return node }
            if let children = node.children, let found = search(in: children) {
                return found
            }
        }
        return nil
    }
    return search(in: objectTree)
}

5. 处理单元格点击事件

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    guard let selectedNode = dataSource.itemIdentifier(for: indexPath) else {
        tableView.deselectRow(at: indexPath, animated: true)
        return
    }
    
    // 没有子节点的单元格不处理展开折叠
    guard selectedNode.children != nil else {
        tableView.deselectRow(at: indexPath, animated: true)
        return
    }
    
    // 切换展开状态
    if expandedNodeIds.contains(selectedNode.id) {
        expandedNodeIds.remove(selectedNode.id)
    } else {
        expandedNodeIds.insert(selectedNode.id)
    }
    
    // 更新表格
    updateTableView()
    tableView.deselectRow(at: indexPath, animated: true)
}

方案优势

  • 彻底解决嵌套折叠问题:递归生成的数据源会自动隐藏所有未展开的嵌套子节点
  • 避免崩溃风险:无需手动计算插入/删除的行号,Diffable数据源自动保证表格与数据源一致性
  • 状态跟踪可靠:用节点唯一ID记录展开状态,不受展示位置变化影响
  • 动画流畅:Diffable数据源提供原生展开折叠动画,体验更好

内容的提问来源于stack exchange,提问作者Ganaraj Savant

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 04:09:56