Search code examples
iosswiftxcodecgaffinetransformuianimation

CGAffineTransform: Apply translate and scale at the same time


I have two UILabels. A bigger on the left side of the screen and a smaller on the right side.

I'm trying to use CGAffineTransform to animate moving the smaller label into the place of the bigger one and scale it to the same size, and move the bigger one out of the screen.

I'm not actually moving the labels, after the animation is complete, I change the text property on the labels and I set their transforms to identity.

My problem is that I don't know how to calculate the exact x and y values that I have to translate my smaller label. I think the values I have are not accurate because I scale the label the same time I translate it, and the tx and ty values are calculated with the non-scaled size of the smaller label.

What I do currently: tx: the width of the bigger label + the distance between it and the smaller label, ty: the distance between the centres of the two labels on the y axis


Solution

  • There are various ways to do this - here's one...

    Start by calculating the translation values and the scale values, the concatenate them:

        let translation = CGAffineTransform(translationX: xMove, y: yMove)
        let scaling = CGAffineTransform(scaleX: xScale, y: yScale)
        
        let fullTransform = scaling.concatenating(translation)
        
    

    Here's a complete example... we add two labels with different font sizes, locations and background colors (to make it easy to see). Tap anywhere to run the transform animation:

    class ViewController: UIViewController {
    
        let labelA = UILabel()
        let labelB = UILabel()
        
        var aTop: NSLayoutConstraint!
        var aLeading: NSLayoutConstraint!
    
        var bTop: NSLayoutConstraint!
        var bLeading: NSLayoutConstraint!
        
        override func viewDidLoad() {
            super.viewDidLoad()
    
            [labelA, labelB].forEach { v in
                v.translatesAutoresizingMaskIntoConstraints = false
                view.addSubview(v)
            }
            
            labelA.text = "Label A"
            labelB.text = "Label B"
            
            labelA.backgroundColor = .green
            labelB.backgroundColor = .cyan
            
            labelA.font = .systemFont(ofSize: 40.0)
            labelB.font = .systemFont(ofSize: 20.0)
    
            // respect safe area
            let g = view.safeAreaLayoutGuide
            
            aTop = labelA.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0)
            aLeading = labelA.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0)
            
            bTop = labelB.topAnchor.constraint(equalTo: g.topAnchor, constant: 300.0)
            bLeading = labelB.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 240.0)
            
            NSLayoutConstraint.activate([
                aTop, aLeading,
                bTop, bLeading,
            ])
            
            let t = UITapGestureRecognizer(target: self, action: #selector(self.doAnim(_:)))
            view.addGestureRecognizer(t)
            
        }
        
        @objc func doAnim(_ g: UITapGestureRecognizer?) -> Void {
            
            let targetPoint = labelA.center
            let originPoint = labelB.center
            
            let xMove = targetPoint.x - originPoint.x
            let yMove = targetPoint.y - originPoint.y
    
            let xScale = labelA.frame.width / labelB.frame.width
            let yScale = labelA.frame.height / labelB.frame.height
            
            let translation = CGAffineTransform(translationX: xMove, y: yMove)
            let scaling = CGAffineTransform(scaleX: xScale, y: yScale)
            
            let fullTransform = scaling.concatenating(translation)
            
            UIView.animate(withDuration: 1.0, animations: {
                self.labelB.transform = fullTransform
            }) { [weak self] b in
                guard let self = self else { return }
                self.labelB.transform = .identity
                self.labelB.font = self.labelA.font
                self.bTop.constant = self.aTop.constant
                self.bLeading.constant = self.aLeading.constant
            }
    
        }
        
    }