如何实现带树形结构子单元格的UITableView展开/折叠功能?
树形UITableView展开折叠解决方案
你的核心问题在于直接修改原始树形数据源+用行号跟踪展开状态,这种方式不仅无法处理嵌套子节点的折叠,还极易因行号计算错误导致表格崩溃。下面是更可靠的优化方案:
核心思路
- 保留原始树形数据不变,作为真实数据来源
- 用节点唯一ID跟踪展开状态(而非行号,行号会随展开折叠动态变化)
- 动态生成扁平化展示数据源,根据展开状态递归遍历原始树生成
- 可选使用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
相关产品推荐
相关产品推荐

