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

Time:11-03

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!

CodePudding user response:

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()
            })
        }
    }
}
  • Related