4

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.

Sweeper
  • 213,210
  • 22
  • 193
  • 313

2 Answers2

3

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)
}
Sandeep
  • 20,908
  • 7
  • 66
  • 106
  • That's weird because if you have a point at (x, y), and you want it to go back to the origin, shouldn't you do a transformation of (-x, -y)? Where is mistake in my understanding? – Sweeper Mar 02 '19 at 10:12
  • I see, so it's in the order of matrix multiplication! The code reads "do A then B then C" but it actually does C, B then A, which is really confusing. I actually found a solution that doesn't need these transformations. – Sweeper Mar 02 '19 at 10:48
  • Yes it seems to be in reverse order. It first applies origintransaltion then applies rotation to it and then apply back translation. – Sandeep Mar 02 '19 at 11:02
0

Since I only want to rotate the view by 90 degrees, the width and height of the initial frame will be the reverse of the width and height of the rotated frame.

The centers of the initial frame and rotated are the same, so we can set the center of the label to the centre of the desired frame before the transformation.

    let desiredFrame = CGRect(x: 10, y: 100, width: 50, height: 100)
    let label1 = UIView(frame: desiredFrame)
    // get the centre of the desired frame
    // this is also equal to label1.center
    let center = CGPoint(x: desiredFrame.midX, y: desiredFrame.midY)
    let label2Frame = CGRect(x: 0, y: 0, width: desiredFrame.height, height: desiredFrame.width)
    let label2 = UIView(frame: label2Frame)
    view.addSubview(label1)
    view.addSubview(label2)
    label1.backgroundColor = .red
    label2.backgroundColor = .blue
    label2.center = center // set the centre of label2 before the transform
    label2.transform = CGAffineTransform(rotationAngle: -.pi / 2)
Sweeper
  • 213,210
  • 22
  • 193
  • 313