Search code examples
iosuikitnslayoutconstraint

UIKit: how to animate a constraint change with a programmatically created constraint?


I have followed this answer to instantiate a view from a XIB file:

extension UIView {    
    class func fromNib(named: String? = nil) -> Self {
        let name = named ?? "\(Self.self)"
        guard let nib = Bundle.main.loadNibNamed(name, owner: nil, options: nil) else {
            fatalError("missing expected nib named: \(name)")
        }
        guard let view = nib.first as? Self else {
            fatalError("view of type \(Self.self) not found in \(nib)")
        }
        return view
    }
}

Now I want to create a view with an adaptive height depending on its content:

class CustomTopView: UIView {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subtitleLabel: UILabel!
    
    static func instance(withTitle title: NSAttributedString, subtitle: NSAttributedString) -> CustomTopView {
        let customTopView = CustomTopView.fromNib()
        customTopView.titleLabel.attributedText = title
        customTopView.subtitleLabel.attributedText = subtitle
        return customTopView
    }
}

And what I would like to do is to display this view animating from the top of the screen, like a notification popup. I wrote this:

extension UIView {
    func showCustomTopView(withTitle title: NSAttributedString, subtitle: NSAttributedString) {
        let customTopView = CustomTopView.instance(withTitle: title, subtitle: subtitle)
        addSubview(customTopView)
        customTopView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            customTopView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
            customTopView.constraint(equalTo: customTopView.trailingAnchor, constant: 20.0),
        ])
        customTopView.layoutIfNeeded() // *
        NSLayoutConstraint.activate([customTopView.topAnchor.constraint(equalTo: topAnchor, constant: -customTopView.bounds.height)])
    }
}

I have two issues here. The main one is that I don't know how, from this, to perform the animation I want so the view ends up visible. I tried this after the last NSLayoutConstraint.activate(...):

UIView.animate(withDuration: 0.5) {
    self.topAnchor.constraint(equalTo: customTopView.topAnchor, constant: 100.0).isActive = true
    self.layoutIfNeeded()
}

But instead of displaying as expected, the popup starts 100px from the top of the screen, then goes up and disappears. What am I doing wrong?

Also, I am not sure of the line marked with // *: I wrote this line to fetch the correct customTopView height, but I'm not sure that's the right way to do it.

Thank you for your help!


Solution

  • One approach:

    • create one constraint setting the Bottom of customTopView above the Top of the view
    • create a second constraint setting the Top of customTopView below the Top of the view
    • add customTopView as a subview, activating the first constraint
    • animate customTopView into view by deactivating the first constraint and activating the second constraint

    Here's a modified version of your showCustomTopView(...) extension, using a UILabel as customTopView -- should work fine with your CustomTopView.instance(withTitle: ...):

    extension UIView {
        func showCustomTopView(withTitle title: NSAttributedString, subtitle: NSAttributedString) {
            let customTopView = UILabel()
            customTopView.text = title.string
            customTopView.backgroundColor = .green
            addSubview(customTopView)
            customTopView.translatesAutoresizingMaskIntoConstraints = false
            let g = self.safeAreaLayoutGuide
            // constraint: Bottom of customTopView to Top of self Minus 8-pts
            let cvBot = customTopView.bottomAnchor.constraint(equalTo: topAnchor, constant: -8.0)
            // constraint: Top of customTopView to Top of self Plus 8-pts
            let cvTop = customTopView.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0)
            NSLayoutConstraint.activate([
                customTopView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20.0),
                customTopView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20.0),
                // activate cvBot, so customTopView is 8-pts above the top of self
                cvBot,
            ])
            // execute this async, so the initial view position is set
            DispatchQueue.main.async {
                // deactivate the Bottom constraint
                cvBot.isActive = false
                // activate the Top constraint
                cvTop.isActive = true
                // animate it into view
                UIView.animate(withDuration: 0.5, animations: {
                    self.layoutIfNeeded()
                })
            }
        }
    }