Search code examples
iosswiftrotationcgaffinetransform

Swift CGAffineTransform rotation jumps


I used the information from How to rotate an UIImageView using TouchesMoved in my own project. But I found a problem, and I can’t figure out how to solve it.

The view in the original post has the same problem but with the small view it is almost not noticeable. A made this example code. See how the view “jumps”. The touch point is always left of the anchor point. How can I avoid this jump.

class ViewController: UIViewController {
    var myView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        myView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 200))
        myView.center = self.view.center
        myView.isUserInteractionEnabled = true
        myView.backgroundColor = UIColor.red
        self.view.addSubview(myView)
        myView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.0)

    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch: UITouch = touches.first!

        if touch.view === myView {
            let position = touch.location(in: self.view)
            let target = myView.center
            let angle = atan2(target.y-position.y, target.x-position.x)
            myView.transform = CGAffineTransform(rotationAngle: angle)
        }
    }
}

Solution

  • You have two major problems:

    • You are not taking into account where the original touch was. You need to record that information in touchesBegan and work out the angle of your rotation transform with respect to that.

    • You are saying let target = myView.center as if this were the point we are rotating around. But it isn't, because you moved the anchorPoint of myView.

    Here's working code (taken from my book) that lets you drag to rotate myView around its own center:

    class ViewController: UIViewController {
        var myView: UIView!
        override func viewDidLoad() {
            super.viewDidLoad()
            myView = UIView(frame: CGRect(x: 0, y: 0, width: 400, height: 200))
            myView.center = self.view.center
            myView.isUserInteractionEnabled = true
            myView.backgroundColor = UIColor.red
            self.view.addSubview(myView)
        }
        func pToA (_ t:UITouch) -> CGFloat {
            let loc = t.location(in: myView)
            let c = myView.convert(myView.center, from:myView.superview!)
            return atan2(loc.y - c.y, loc.x - c.x)
        }
        var initialAngle = CGFloat(0)
        var angle = CGFloat(0)
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            self.initialAngle = pToA(touches.first!)
        }
        override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
            let ang = pToA(touches.first!) - self.initialAngle
            let absoluteAngle = self.angle + ang
            myView.transform = myView.transform.rotated(by: ang)
            self.angle = absoluteAngle
        }
    }
    

    But if you really want to rotate around a different point (moving the anchor point as your code does), you'd need to adjust that code accordingly.