LoginSignup
12
9

More than 5 years have passed since last update.

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

Posted at

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

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

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

IMB_vw5oer.gif

結論から言うと、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

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

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
    }
}
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()
        }
    }
}
12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9