Fogmeister
Fogmeister

Reputation: 77631

Animating a view with AutoLayout but it changes without actually animating

I have a custom view subclass that I will provide all the code of for clarity. I have highlighted the relevant parts below.

Note: I know how to animate views using AutoLayout. The problem is not writing the animation code. The problem is that it updates the view but doesn't actually animate anything. It just jumps to the new size.

class ExpandingButtonView: UIView {
    let titleLabel: UILabel = {
        let l = UILabel()
        l.translatesAutoresizingMaskIntoConstraints = false
        l.textColor = .white
        l.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .vertical)
        l.setContentHuggingPriority(UILayoutPriorityRequired, for: .vertical)
        return l
    }()

    let buttonStack: UIStackView = {
        let s = UIStackView()
        s.translatesAutoresizingMaskIntoConstraints = false
        s.axis = .vertical
        s.spacing = 8
        s.isLayoutMarginsRelativeArrangement = true
        s.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
        return s
    }()

    var collapsed: Bool = true {
        didSet {
            animatedCollapsedState()
        }
    }

    lazy var collapsedConstraint: NSLayoutConstraint = {
        return self.bottomAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: 10)
    }()

    lazy var expandedConstraint: NSLayoutConstraint = {
        return self.bottomAnchor.constraint(equalTo: self.buttonStack.bottomAnchor)
    }()

    init(title: String, color: UIColor, buttonTitles: [String]) {
        super.init(frame: .zero)

        translatesAutoresizingMaskIntoConstraints = false
        layer.cornerRadius = 8
        clipsToBounds = true
        backgroundColor = color

        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(toggleCollapsed))
        tapGestureRecognizer.numberOfTapsRequired = 1
        tapGestureRecognizer.numberOfTouchesRequired = 1
        addGestureRecognizer(tapGestureRecognizer)

        titleLabel.text = title
        addSubview(titleLabel)
        addSubview(buttonStack)

        buttonTitles.forEach {
            let button = UIButton(type: .system)
            button.translatesAutoresizingMaskIntoConstraints = false
            button.backgroundColor = UIColor(white: 1.0, alpha: 0.5)
            button.setTitle($0, for: .normal)
            button.tintColor = .white
            button.layer.cornerRadius = 4
            button.clipsToBounds = true
            button.titleLabel?.font = .boldSystemFont(ofSize: 17)
            button.setContentCompressionResistancePriority(UILayoutPriorityRequired, for: .vertical)

            buttonStack.addArrangedSubview(button)
        }

        NSLayoutConstraint.activate([
            collapsedConstraint,
            titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10),
            titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 10),
            titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 10),
            titleLabel.bottomAnchor.constraint(equalTo: buttonStack.topAnchor),
            buttonStack.leadingAnchor.constraint(equalTo: leadingAnchor),
            buttonStack.trailingAnchor.constraint(equalTo: trailingAnchor),
            ])
    }

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

    func toggleCollapsed() {
        collapsed = !collapsed
    }

    func animatedCollapsedState() {
        UIView.animate(withDuration: 1) {
            self.collapsedConstraint.isActive = self.collapsed
            self.expandedConstraint.isActive = !self.collapsed
            self.layoutIfNeeded()
        }
    }
}

It has two states...

  1. Collapsed...

enter image description here

  1. Expanded...

enter image description here

When you tap it the tapGestureRecognizer toggles the collapsed value which triggers the didSet which then animates the UI.

The animating function is...

func animatedCollapsedState() {
    UIView.animate(withDuration: 1) {
        self.collapsedConstraint.isActive = self.collapsed
        self.expandedConstraint.isActive = !self.collapsed
        self.layoutIfNeeded()
    }
}

However... it is not animating. It just jumps to the new size without actually animating.

I have removed another part of the view for the question. There is also a background image view that fades in/out during the UI change. That DOES animate. So I'm not quite sure what's going on here?

I have also tried moving the constraint updates out of the animation block and also tried running layoutIfNeeded() before updating them.

In all cases it does the same thing jumping to the new size.

Upvotes: 0

Views: 48

Answers (1)

André Slotta
André Slotta

Reputation: 14030

You have to call layoutIfNeeded() on the view's superview.

func animatedCollapsedState() {
    self.collapsedConstraint.isActive = self.collapsed
    self.expandedConstraint.isActive = !self.collapsed
    UIView.animate(withDuration: 1) {
        self.superview?.layoutIfNeeded()
    }
}

Upvotes: 1

Related Questions