iosswiftuiimageviewcgaffinetransformrotatetransform

Swift - CGAffineTransform rotation


I have an UIImageView to rotate in a draw function.

public var progress: CGFloat = 0 {
    didSet {
        setNeedsDisplay()
    }
}

override public func draw(_ rect: CGRect) {
    rotateIcon(by: progress)
}

private func rotateIcon(by angle: CGFloat) {
    if imageView == nil {
        imageView = UIImageView(image: UIImage(named: "bt_info"))
        imageView?.center = center
        imageView?.frame.size = CGSize(width: 24.0, height: 24.0)
        addSubview(imageView!)
    }
    imageView?.transform = CGAffineTransform(rotationAngle: angle)
}

Below is the output attached for the above snippet.

enter image description here

When I set the frame size, the imageView shrink and expands as it rotates. I would like to maintain the size as the imageView rotates. What am I missing?


Solution

  • OK - the code you posted in your question does not match the code in your Dropbox project...

    In your project, you're doing this:

    private func rotateScannerIcon(by angle: CGFloat) {
        if testView == nil {
            testView = UIView()
            testView?.backgroundColor = .red
            addSubview(testView!)
        }
        
        let centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
        testView?.center = centerPoint
        testView?.frame.size = CGSize(width: 24.0, height: 24.0)
    
        testView?.transform = CGAffineTransform(rotationAngle: angle)
    }
    

    With that code, you are changing the frame size after a rotation transform has been applied... and the actual view is no longer "square."

    If you move the position / size code inside your "create" block:

    private func rotateScannerIcon(by angle: CGFloat) {
        if testView == nil {
            testView = UIView()
            testView?.backgroundColor = .red
            addSubview(testView!)
    
            let centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
            testView?.center = centerPoint
            testView?.frame.size = CGSize(width: 24.0, height: 24.0)
        }
        
        testView?.transform = CGAffineTransform(rotationAngle: angle)
    }
    

    The "squeezing" of the frame no longer happens.

    Overriding draw() is probably not the best approach, though. That is primarily done when UIKit view updates are not sufficient to handle the layout - such as when using stroke/fill on paths.

    So, I would suggest a couple changes.

    Create your subview (UIView or UIImageView or whatever) when the custom view subclass is initialized, and size/position it with auto-layout constraints.

    Then, when you update the progress property, apply the rotation transform.

    As an example, here's a modified version of your TestView class:

    public class TestView: UIView {
        
        public var progress: CGFloat = 0 {
            didSet {
                rotateScannerIcon(by: (progress * CGFloat(Double.pi)))
            }
        }
        
        internal var testView: UIView!
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        func commonInit() {
            // create the view (or image view)
            testView = UIView()
            testView.backgroundColor = .red
            
            // add it to self
            addSubview(testView)
            
            // let's use auto-layout constraints
            testView.translatesAutoresizingMaskIntoConstraints = false
            
            NSLayoutConstraint.activate([
                // constrain the view to 24 x 24, centered in self
                testView.widthAnchor.constraint(equalToConstant: 24.0),
                testView.heightAnchor.constraint(equalTo: testView.widthAnchor),
                testView.centerXAnchor.constraint(equalTo: centerXAnchor),
                testView.centerYAnchor.constraint(equalTo: centerYAnchor),
            ])
            
        }
    
        private func rotateScannerIcon(by angle: CGFloat) {
            testView.transform = CGAffineTransform(rotationAngle: angle)
        }
    
    }