Search code examples
iosswiftuilabelnslayoutconstraintcgaffinetransform

Maintaining the location of a view when changing its anchor point


I have a UILabel created programmatically and set with constraints:

NSLayoutConstraint.activate([
            theLabel.topAnchor.constraint(equalTo: otherView.topAnchor),
            theLabel.leadingAnchor.constraint(equalTo: otherView.trailingAnchor, constant: otherView.frame.width/2),
            theLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
            theLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2),
])

I then wanted to grow the label onto the scene:

theLabel.transform = CGAffineTransform(scaleX: 0.0001, y: 0.0001)
theLabel.isHidden = false

UIView.animate(withDuration: 0.8, delay: 0.0, options: UIViewAnimationOptions(), animations: {
    self.theLabel.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) //or CGAffineTransform.identity
}, completion: nil)

But the label is left aligned, and it grows from the center, so I changed the anchor point to fix things, but that resulted in the label appearing further to the right than it should be. (I understand this is because I am now describing its position by the left side of the view and no longer by the center)

theLabel.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)

I tried to just replace its frame and its center point after setting the anchorPoint (separately, I tried first the frame, then the center) but neither of these solutions made any difference to how the view displayed in app:

let oldFrame = theLabel.frame
theLabel.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
theLabel.frame = oldFrame

let oldCenter = theLabel.center
theLabel.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
theLabel.center = oldCenter

Any other suggestions or explanations to help me understand how I can effectively preserve the location of the view while changing its anchor point? Thanks all!


Solution

  • One approach: save the label's leading constraint in a variable, and then change its constant to account for the change in anchorPoint.

    You should be able to run this as-is. Tap the button to animate the label.

    class LabelTransformViewController: UIViewController {
    
        let btn: UIButton = {
            let b = UIButton()
            b.translatesAutoresizingMaskIntoConstraints = false
            b.setTitle("Tap Me", for: .normal)
            b.backgroundColor = .red
            return b
        }()
    
        let theLabel: UILabel = {
            let v = UILabel()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = UIColor.cyan
            v.text = "This is a label"
            return v
        }()
    
        let otherView: UIView = {
            let v = UIView()
            v.translatesAutoresizingMaskIntoConstraints = false
            v.backgroundColor = UIColor.yellow
            return v
        }()
    
        var leadingConstraint = NSLayoutConstraint()
    
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = UIColor(red: 1.0, green: 0.85, blue: 0.5, alpha: 1.0)
    
            view.addSubview(btn)
    
            btn.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
    
            btn.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
            btn.topAnchor.constraint(equalTo: view.topAnchor, constant: 20.0).isActive = true
    
            view.addSubview(otherView)
    
            NSLayoutConstraint.activate([
                otherView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100.0),
                otherView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20.0),
                otherView.widthAnchor.constraint(equalToConstant: 30.0),
                otherView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2),
                ])
    
            view.addSubview(theLabel)
    
            NSLayoutConstraint.activate([
                theLabel.topAnchor.constraint(equalTo: otherView.topAnchor),
                theLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
                theLabel.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.2),
                ])
    
            leadingConstraint = theLabel.leadingAnchor.constraint(equalTo: otherView.trailingAnchor, constant: otherView.frame.width/2)
            leadingConstraint.isActive = true
    
            // presumably, you would start with theLabel hidden
            // but we're leaving it visible so we can see its original size and position
    //      theLabel.isHidden = true
    
        }
    
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)
    
            // move the anchorPoint from the label frame's center (the default)
            // to the left edge, vertical center
            theLabel.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5)
    
            // adjust the label's leading constraint by 1/2 of its width
            // to account for the anchorPoint change
            // need this in viewDidAppear so the frame is already set
            leadingConstraint.constant -= (theLabel.frame.size.width * 0.5)
        }
    
        @objc func didTap(_ sender: Any?) -> Void {
    
            theLabel.transform = CGAffineTransform(scaleX: 0.0001, y: 0.0001)
            theLabel.isHidden = false
    
            UIView.animate(withDuration: 0.8, delay: 0.0, options: UIViewAnimationOptions(), animations: {
                self.theLabel.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) //or CGAffineTransform.identity
            }, completion: nil)
    
        }
    
    }