Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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()
        }
    }
}
iincho
フリーランスでiPhoneアプリ開発をしています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away