为 UITableViewCell 高度变化添加动画

如何让 UITableView 的 cell 高度动态变化且有动画效果呢?

如题, 要想达到目的, 我目前总结有两种方式:

  1. 使用 UITableView 的 func reloadRows(at indexPaths: [IndexPath], with animation: UITableView.RowAnimation) 方法

    • 优点: 调用简单, 只需要针对指定 cell 做出高度变更, 然后将该 cell 的 indexPath 传入此方法即可
    • 缺点:
      • 会调用针对该 cell 调用 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell 等代理方法
      • 刷新时界面会有闪烁现象
  2. 使用 UITableView 的 func beginUpdates()func endUpdates() 方法

    • 优点: 可以自定义动画时长与效果, 更加灵活, 在对指定 cell 做出改变后, 调用此方法会触发所有 cell 的高度计算并刷新, 且高度变化时有动画效果

      在 iOS 11 之后, 我们可以用 func performBatchUpdates(_ updates: (() -> Void)?, completion: ((Bool) -> Void)? = nil) 方法来代替 beginUpdatesendUpdates 方法

综上, 我在实际项目中选择方法2作为动态高度动画方案, 具体效果如下:

himg

案例

使用 beginUpdatesendUpdates (或 performBatchUpdates) 的实际案例代码如下:

  • TestTableViewCellExpandableVC.swift

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    import UIKit

    class TestTableViewCellExpandableVC: UIViewController {

    private var tableView: UITableView!

    override func viewDidLoad() {
    super.viewDidLoad()
    initView()
    }
    }

    // MARK: - UITableView Data Source
    extension TestTableViewCellExpandableVC: UITableViewDataSource {
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = (tableView.dequeueReusableCell(withIdentifier: "Cell") as? TestTableViewCellExpandableCell) ??
    .init(style: .default, reuseIdentifier: "Cell")

    cell.setContent(with: "Row \(indexPath.row)") { [weak cell, weak tableView] in
    if #available(iOS 11.0, *) {
    tableView?.performBatchUpdates {
    cell?.updateHeight()
    }
    } else {
    cell?.updateHeight()
    tableView?.beginUpdates()
    tableView?.endUpdates()
    }
    }

    return cell
    }

    func numberOfSections(in tableView: UITableView) -> Int {
    return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 3
    }

    }

    // MARK: UI

    extension TestTableViewCellExpandableVC {
    private func initView() {

    tableView = .init()
    tableView.dataSource = self
    tableView.separatorStyle = .none
    view.addSubview(tableView)
    tableView.snp.makeConstraints { make in
    make.top.equalToSuperview()
    make.left.right.bottom.equalToSuperview()
    }

    }
    }
  • TestTableViewCellExpandableCell.swift

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    import UIKit

    class TestTableViewCellExpandableCell: UITableViewCell {

    enum HeightType {
    case small
    case medium
    case big

    var height: CGFloat {
    switch self {
    case .small: return 100
    case .medium: return 200
    case .big: return 300
    }
    }

    var next: HeightType {
    switch self {
    case .small: return .medium
    case .medium: return .big
    case .big: return .small
    }
    }
    }

    // MARK: Subviews
    private var titleLb: UIButton!

    // MARK: Data
    private var heightType: HeightType = .small
    private var tapClosure: () -> Void = {}

    // MARK: Life Cycle

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    initUI()
    }

    required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
    }
    }

    // MARK: - Custom Method

    extension TestTableViewCellExpandableCell {
    func setContent(with title: String?, tapClosure: @escaping () -> Void) {
    titleLb.setTitle(title, for: .normal)
    self.tapClosure = tapClosure
    }

    func updateHeight() {
    heightType = heightType.next
    titleLb.snp.updateConstraints { make in
    // 这里必须使用 .low 以上的优先级, 否则会约束报错
    make.height.equalTo(heightType.height).priority(.high)
    }
    }

    @objc private func tap() {
    tapClosure()
    }
    }


    // MARK: - UI

    extension TestTableViewCellExpandableCell {
    private func initUI() {
    backgroundColor = UIColor.gray

    titleLb = .init()
    titleLb.addTarget(self, action: #selector(tap), for: .touchUpInside)
    titleLb.setTitleColor(.white, for: .normal)
    titleLb.backgroundColor = UIColor.red
    contentView.addSubview(titleLb)

    titleLb.snp.makeConstraints { make in
    make.height.equalTo(heightType.height).priority(.high)
    make.width.equalTo(200)
    make.centerX.equalToSuperview()
    make.top.bottom.equalToSuperview().inset(5)
    }

    let bottomLine = UIView()
    bottomLine.backgroundColor = .black
    contentView.addSubview(bottomLine)
    bottomLine.snp.makeConstraints { make in
    make.left.right.equalToSuperview().inset(5)
    make.bottom.equalToSuperview()
    make.height.equalTo(0.5)
    }
    }
    }

Reference