Swift实现UITableViewCell点击按钮展开高度约束的动画问题
哦,这个问题我太熟了——用reloadData()确实会把所有动画都冲掉,因为它是直接粗暴地刷新整个表格,完全跳过了系统的动画流程。下面给你一套完整的解决方案,保证能实现平滑的展开/折叠动画:
核心思路
要实现带动画的单元格展开,关键是不要全局刷新表格,而是让表格只更新目标单元格的高度,同时配合cell内部的约束动画。具体要做这几件事:
- 跟踪每个单元格的展开状态(避免刷新后丢失状态)
- 用表格的
beginUpdates()+endUpdates()触发高度变化动画 - 在cell内部通过修改约束来控制展开/折叠,而非依赖表格刷新
完整代码实现
第一步:ViewController层(管理表格和展开状态)
class YourTableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet weak var tableView: UITableView! // 用Set来记录展开的单元格索引,方便快速查找和修改 private var expandedIndexPaths: Set<IndexPath> = [] override func viewDidLoad() { super.viewDidLoad() // 开启自动高度计算,让表格根据cell内容自动调整高度 tableView.rowHeight = UITableView.automaticDimension // 设置预估高度,帮助表格提前计算布局,避免跳动 tableView.estimatedRowHeight = 120 // 注册自定义cell(如果不是在Storyboard里绑定的话) tableView.register(YourExpandableCell.self, forCellReuseIdentifier: "ExpandableCell") } // MARK: - UITableView DataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return yourDataSource.count // 替换成你的数据源数量 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ExpandableCell", for: indexPath) as! YourExpandableCell // 给cell设置内容和当前展开状态 cell.configure(with: yourDataSource[indexPath.row], isExpanded: expandedIndexPaths.contains(indexPath)) // 绑定按钮点击事件,用闭包通知控制器切换状态 cell.onExpandTapped = { [weak self] in self?.toggleCellExpansion(at: indexPath) } return cell } // MARK: - 处理展开/折叠逻辑 private func toggleCellExpansion(at indexPath: IndexPath) { // 更新展开状态集合 if expandedIndexPaths.contains(indexPath) { expandedIndexPaths.remove(indexPath) } else { expandedIndexPaths.insert(indexPath) } // 关键!用这两个方法包裹,表格会自动计算高度变化并添加平滑动画 tableView.beginUpdates() tableView.endUpdates() // 可选:如果想让展开的单元格滚动到可视区域 // tableView.scrollToRow(at: indexPath, at: .top, animated: true) } }
第二步:自定义Cell层(处理内部约束动画)
class YourExpandableCell: UITableViewCell { // 假设你的container view有一个高度约束(在Storyboard里绑定好) @IBOutlet weak var containerHeightConstraint: NSLayoutConstraint! @IBOutlet weak var expandButton: UIButton! // 用闭包通知控制器按钮被点击 var onExpandTapped: (() -> Void)? // 配置cell内容和展开状态 func configure(with data: YourDataModel, isExpanded: Bool) { // 先设置你的cell内容,比如标题、文本等 // titleLabel.text = data.title // 更新展开状态和约束 self.isExpanded = isExpanded } // 展开状态属性,didSet里处理约束更新 var isExpanded: Bool = false { didSet { // 调整container view的高度约束,替换成你需要的展开/折叠高度 containerHeightConstraint.constant = isExpanded ? 200 : 80 // 更新按钮文字/图标(可选) expandButton.setTitle(isExpanded ? "收起" : "展开", for: .normal) // 触发布局更新,必须在主线程执行,保证动画平滑 DispatchQueue.main.async { self.layoutIfNeeded() } } } // 按钮点击事件 @IBAction func expandButtonTapped(_ sender: UIButton) { onExpandTapped?() } }
额外注意事项
- 约束正确性:确保你的cell内部所有子视图都有完整的自动布局约束,包括top、bottom、leading、trailing,这样
UITableView.automaticDimension才能正确计算高度。 - 替代方案(不用自动高度):如果不想用自动高度,可以在ViewController里实现
heightForRowAt方法,根据展开状态返回固定高度:func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return expandedIndexPaths.contains(indexPath) ? 280 : 100 } - 批量折叠/展开:如果需要点击一个按钮展开所有单元格,只需要清空或填充
expandedIndexPaths集合,然后调用beginUpdates()+endUpdates()即可。 - 避免循环引用:在闭包中一定要用
weak self,防止ViewController和Cell互相引用导致内存泄漏。
内容的提问来源于stack exchange,提问作者Niall Kiddle
相关产品推荐
相关产品推荐

