UITableViewCell
の高さが変わるアニメーションを行うには、UITableView.beginUpdates
UITableView.endUpdates
でCell高さ変更処理を挟むことで実現できます。
tableView.beginUpdates()
// Cell高さが変更されるような処理
tableView.endUpdates()
ここで、Cell内部にある、VerticalなUIStackView
の一部要素の表示非表示を切り替えてアニメーションする場合、UITableView
側のアニメーション設定と合わせて、UIStackView
側のアニメーション設定が必要で、Duration設定を正しく設定しないと、アニメーションが安定しません。
結論から言うと、UITableView
側のアニメーションのDuration: 0.3
と同じ時間となるよう、UIStackView
側のDurationを調整すると、アニメーションが安定します。
上記Gifの実装では、VerticalなUIStackView
へ5つのUILabel
を追加し、そのうち、2〜4番目のUILabel
の表示非表示を切り替えています。5つのCellに対して、0.1~0.5のDuration
を設定しています。
UITableView
側のDuration
と揃っていないCellのアニメーションはご覧のとおり、アニメーション中に一部の'UILabel`が消えたり上下にぐらついて見えます。
Githubにサンプルコードをおいておきました。
https://github.com/iincho/UITableViewAnimation
以下、サンプルコードの一部です。
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
}
}
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()
}
}
}