Search code examples
iosswiftcgaffinetransformcgrect

How to rotate a CGRect about its centre?


Background:

I want to show a label that is rotated 90 degrees clockwise. The rotated label should be in a certain frame that I specify. Let's say (10, 100, 50, 100).

I know that I can rotate a view by using CGAffineTransform. I also know that I should not set the frame property after a transformation is done, according to the docs.

When the value of this property is anything other than the identity transform, the value in the frame property is undefined and should be ignored.

I tried to set the frame after the transform and although it worked, I don't want to do something that the docs told me not to.

label2.transform = CGAffineTransform(rotationAngle: -.pi / 2)
label2.frame =  CGRect(x: 10, y: 100, width: 50, height: 100)

Then I thought, I could do this:

  1. create a CGRect of the frame that I want
  2. rotate that rect 90 degrees anticlockwise
  3. set that rect as the frame of the label
  4. rotate the label 90 degrees clockwise

And then the label would be in the desired frame.

So I tried something like this:

    let desiredFrame = CGRect(x: 10, y: 100, width: 50, height: 100) // step 1
    // I am using UIViews here because I am just looking at their frames
    // whether it is a UIView or UILabel does not matter
    // label1 is a view that is in the desired frame but not rotated
    // If label2 has the correct frame, it should completely cover label1
    let label1 = UIView(frame: desiredFrame)
    let label2Frame = rotateRect(label1.frame) // step 2
    let label2 = UIView(frame: label2Frame) // step 3
    view.addSubview(label1)
    view.addSubview(label2)
    label1.backgroundColor = .red
    label2.backgroundColor = .blue
    label2.transform = CGAffineTransform(rotationAngle: -.pi / 2) // step 4

Where rotateRect is declared like this:

func rotateRect(_ rect: CGRect) -> CGRect {
    return rect.applying(CGAffineTransform(rotationAngle: .pi / 2))
}

This didn't work. label2 does not overlap label1 at all. I can't even see label2 at all on the screen.

I suspect that this is because the applying method in CGRect rotates the rect about the origin, instead of the centre of the rect. So I tried to first transform the rect to the origin, rotate it, then transform it back, as this post on math.SE said:

func rotateRect(_ rect: CGRect) -> CGRect {
    let x = rect.x
    let y = rect.y
    let transform = CGAffineTransform(translationX: -x, y: -y)
                        .rotated(by: .pi / 2)
                        .translatedBy(x: x, y: y)
    return rect.applying(transform)
}

However, I still cannot see label2 on the screen.


Solution

  • I think that the order of your transformations are incorrect. If you do it so, that,

    Translate(x, y) * Rotate(θ) * Translate(-x, -y)

    And using that with your rotateRect seem to work correctly. Since, you are rotating the view by 90 degrees, the blue view completely block red view. Try it out with some other angle and you shall see effect more prominently.

    func rotateRect(_ rect: CGRect) -> CGRect {
        let x = rect.midX
        let y = rect.midY
        let transform = CGAffineTransform(translationX: x, y: y)
                                        .rotated(by: .pi / 2)
                                        .translatedBy(x: -x, y: -y)
        return rect.applying(transform)
    }