UITableViewCell内のVertical UIStackViewをアニメーションさせる

`UITableViewCell`の高さが変わるアニメーションを行うには、`UITableView.beginUpdates` `UITableView.endUpdates`でCell高さ変更処理を挟むことで実現できます。

```swift
    tableView.beginUpdates()
    // Cell高さが変更されるような処理
    tableView.endUpdates()
```

ここで、Cell内部にある、Verticalな`UIStackView`の一部要素の表示非表示を切り替えてアニメーションする場合、`UITableView`側のアニメーション設定と合わせて、`UIStackView`側のアニメーション設定が必要で、Duration設定を正しく設定しないと、アニメーションが安定しません。

![IMB_vw5oer.gif](https://qiita-image-store.s3.amazonaws.com/0/113846/3f82b16c-c361-37c1-3d78-3afe92ff820c.gif)

結論から言うと、`UITableView`側のアニメーションの`Duration: ０．３`と同じ時間となるよう、`UIStackView`側のDurationを調整すると、アニメーションが安定します。

上記Gifの実装では、Verticalな`UIStackView`へ５つの`UILabel`を追加し、そのうち、２〜４番目の`UILabel`の表示非表示を切り替えています。５つのCellに対して、0.1~0.5の`Duration`を設定しています。
`UITableView`側の`Duration`と揃っていないCellのアニメーションはご覧のとおり、アニメーション中に一部の'UILabel`が消えたり上下にぐらついて見えます。

Githubにサンプルコードをおいておきました。
https://github.com/iincho/UITableViewAnimation

以下、サンプルコードの一部です。

```swift:ViewController.swift
import UIKit

struct CellState {
    var isOpen: Bool
    let backgoundColor: UIColor
    let duration: TimeInterval
}

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    private let cellStates: [CellState] = {
       return [
        CellState(isOpen: true, backgoundColor: .white, duration: 0.1),
        CellState(isOpen: true, backgoundColor: .lightGray, duration: 0.2),
        CellState(isOpen: true, backgoundColor: .white, duration: 0.3),
        CellState(isOpen: true, backgoundColor: .lightGray, duration: 0.4),
        CellState(isOpen: true, backgoundColor: .white, duration: 0.5),
        ]
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self

        let className = "AnimationCell"
        let nib = UINib(nibName: className, bundle: nil)
        tableView.register(nib, forCellReuseIdentifier: className)
        
        tableView.estimatedRowHeight = 200
        tableView.rowHeight = UITableView.automaticDimension
        tableView.tableFooterView = UIView()
        tableView.reloadData()
    }
}

extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return cellStates.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "AnimationCell", for: indexPath) as! AnimationCell
        var state = cellStates[indexPath.row]
        cell.refresh(isOpen: state.isOpen, backgroundColor: state.backgoundColor, duration: state.duration)
        cell.toggleButtonTapHandler = { [unowned self] in
            state.isOpen = !state.isOpen
            self.tableView.beginUpdates()
            cell.toggle(isOpen: state.isOpen)
            self.tableView.endUpdates()
        }
        return cell
    }
}
```

```swift:AnimationCell.swift

import UIKit

class AnimationCell: UITableViewCell {
    
    var toggleButtonTapHandler: (() -> Void)?
    
    @IBOutlet private weak var toggleButton: UIButton!
    @IBOutlet private weak var stackView: UIStackView!
    @IBOutlet private weak var label1: UILabel!
    @IBOutlet private weak var label2: UILabel!
    @IBOutlet private weak var label3: UILabel!
    @IBOutlet private weak var label4: UILabel!
    @IBOutlet private weak var label5: UILabel!
    
    private lazy var animationLabels: [UILabel] = [label2, label3, label4]
    private var duration: TimeInterval = 0
    
    @IBAction func toggle(_ sender: Any) {
        toggleButtonTapHandler?()
    }
    
    override func awakeFromNib() {
        super.awakeFromNib()
        selectionStyle = .none
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }
    
    func refresh(isOpen: Bool, backgroundColor: UIColor, duration: TimeInterval) {
        contentView.backgroundColor = backgroundColor
        self.duration = duration
        refreshTitle(isOpen: isOpen)
        animationLabels.forEach { $0.isHidden = isOpen }
    }
    
    private func refreshTitle(isOpen: Bool) {
        let toggleStr = isOpen ? "▼" : "▲"
        let title = "Duration: \(duration) " + toggleStr
        toggleButton.setTitle(title, for: .normal)
    }
    
    func toggle(isOpen: Bool) {
        refreshTitle(isOpen: isOpen)
        
        /// Ainmation
        let opacity: Float = isOpen ? 0.0 : 1.0
        UIView.animate(withDuration: duration) {
            self.animationLabels.forEach { label in
                label.isHidden = isOpen
                label.layer.opacity = opacity
            }
            self.stackView.layoutIfNeeded()
        }
    }
}
```