Why do layer transforms affect a UIView's frame? - ios

Transforming a UIView affects its frame. Transforming a UIView's layer also affects the views frame in the same way. So scaling a view's layer, scales the frame. I'm trying to understand why transforms to the layer affect the views frame (even when view.layer.masksToBounds = NO is set).
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
NSLog(#"Before: %#", NSStringFromCGRect(view.frame));
// Output: {{0, 0}, {50, 50}}
// View transform applied
view.transform = CGAffineTransformMakeScale(2, 2);
NSLog(#"%#", NSStringFromCGRect(view.frame));
// Output: {{-25, -25}, {100, 100}}
// Layer transform applied
view.transform = CGAffineTransformIdentity;
view.layer.transform = CATransform3DMakeScale(2, 2, 1);
NSLog(#"%#", NSStringFromCGRect(view.frame));
// Output: {{-25, -25}, {100, 100}}

You shouldn't look at the frame value once you have a transform, since it's undefined what it contains at that point. This is mentioned in the documentation for the frame property on UIView:
WARNING
If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
If you need that to modify the frame, you have to do so using the center and bounds properties instead.

A frame is a very specific thing.
This rectangle defines the size and position of the view in its superview’s coordinate system. You use this rectangle during layout operations to size and position the view.
Transforms applied to a view effect the origin and size of that view in the superview which is why the view's frame changes.
Transforming subviews will effect the frames of the subviews, but not their superview's frame.
It's worth noting that bounds differs from frame in this respect. The bounds of a view is the origin and size of a view within it's own coordinate system. Transforms should not change a view's bounds, because the transform changes the size and placement of the view for external coordinates, but not the view's internal coordinates.

The frame is a computing property.
Basically, it's synthesized from center and bounds.( To know more, please search for anchorPoint of CALayer).
What's more, when transform is taken into consideration. The frame will be a bounding box that will cover the original box, even rotation or scale is applied.
And the default implementation of hitTest and pointInside will use the final frame, which means you can touch the translated or rotated view normally.

Related

Resizable UIView

I am building a custom UIView that you can rotate and resize. I can resize the UIView by dragging the corners of the UIView. I calculate how much I have dragged then change the frame of the UIView accordingly.
However, I am running into problems once I added a rotation gesture recognizer to the view. If I rotate or apply a transform to the view, I no longer know how to calculate drag distance and change the frame of the view. How could I calculate the width and height change between my new view and the original view when things are put at an added angle or if they have some other transform, like a translation transform?
I thought of possibilities to set the view's transform back to .identity, change the size of the view, then re-apply its transform, but I'm not sure how to actually go about implementing this.
After applying transform you can not use frame
You have two options
1) First Calculate everything using center of your view
2) As you know apply identity and change frame
for point 2 I have added example that might helpful to you
let transform = imageView.transform
imageView.transform = CGAffineTransform.identity
var rect: CGRect = imageView.frame
rect = // Change Rect here
imageView.frame = rect // Assign it
imageView.transform = transform // Apply Transform

CGAffineTransform does not effect autolayout

recently I have been working with CGAffineTransform and CATransform3D on views and layers. When I apply transformation at layer level and especially on view level, shouldn't autolayout updates the frames of related views. For example, consider following layout |H:[ViewA]-[ViewB]|. If I apply transformation on just ViewA.
//We can either
ViewA.transform = CGAffineTransformMakeScale(1.5, 1.5)
ViewA.layer.transform = CATransform3DMakeScale(1.5, 1.5, 1.0)
shouldn't ViewB auto adjust it rect since ViewA height, width or position has changed after transformation. I also tried calling
self.view.layoutIfNeeded()
Transformations don't affect the Frame or Bounds of a view or the frame will be in an undefined state. To illustrate that: image you have a rectangular view with a frame of X:10, Y:10, W:100, H:100 and you rotate that by 45 degrees...what should the frame be?
From Apple's docs about the transformation:
If this property is not the identity transform, the value of the frame
property is undefined and therefore should be ignored.
This explains why AutoLayout does not perform any updates.

Resizing a UIView but drawing done by drawRect also changing size

I am testing a UIView using a UISlider as in the example images below:
I have a custom UIView with a yellow background that draws the gray square, the drawRect method is like so:
-(void)drawRect:(CGRect)rect{
NSLog(#"Draw rect called");
UIBezierPath* squarePath = [UIBezierPath bezierPathWithRect: CGRectMake(10, 10, 100, 100)];
[UIColor.grayColor setFill];
[squarePath fill];
}
And the method for my slide changing value:
- (IBAction)changeValue:(id)sender {
CGAffineTransform transform = CGAffineTransformScale(CGAffineTransformIdentity, self.slider.value, self.slider.value);
self.tableView.transform = transform;
[self.tableView setNeedsDisplay];
}
I dont understand why the square is getting larger. I've noticed that drawRect is called every time the slider is moved. If this happens then why is the square size changing? Shouldn't it remain the same size and just the frame grow with the square in the top left corner?
My second question is, how would I change the code so just the frame grows and the drawing size stays the same? I ask this because actually I want the drawing size to change dynamically using my own code in drawRect.
Any pointers would be really appreciated! thanks!
The reason why the size of the square changes is because you've transformed it. Transformations don't just affect the frame of a view; they will affect the content. The square is getting drawn into its context at its constant size (100x100) and then the transform is stretching it before it gets rendered.
The reason why it's not expanding to the right and down is because by default the anchor point of a transform is the center of the bounds. Thus it'll scale from the center outwards. From the documentation:
The origin of the transform is the value of the center property ...
Transformations aren't intended to be used to simply scale the width and height of your frame. The frame property is for that. Simply store the view's frame in a variable, change its width and height, then set it back. In your drawRect: code you can check the dimensions of the rectangle that's given to you and make your square's width/height a percentage of that.

how does frame of a UILabel gets changed on changing anchor point

I have a UIView object with frame (50, 50, 100, 100).
on this view object, I am adding a label with frame (50, 50, 50, 10)
Then I am changing the anchor point from default (0.5, 0.5) to (1.0, 0.5)
But surprisingly, the frame gets changed after that. Below is the code snippet for the same
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(rect.origin.x, rect.origin.y, rect.size.width/2, 10)];
NSLog(#"Label frame for %d label before anchor point shift is %#", i, NSStringFromCGRect(label.frame));
label.layer.anchorPoint = CGPointMake(1.0, 0.5);
NSLog(#"Label frame for %d label after anchor point shift is %#", i, NSStringFromCGRect(label.frame));
Gives output :
Label frame for 0 label before anchor point shift is {{50, 50}, {50, 10}}
Label frame for 0 label after anchor point shift is {{25, 50}, {50, 10}}
A CALayer has four properties that determine where it appears in its superlayer:
position (which is the same as the view's center property)
bounds (actually only the size part of bounds)
anchorPoint
transform
You will notice that frame is not one of those properties. The frame property is actually derived from those properties. When you set the frame property, the layer actually changes its center and bounds.size based on the frame you provide and the layer's existing anchorPoint.
You create the first layer (by creating the first UILabel, which is a subclass of UIView, and every UIView has a layer), giving it a frame of 50,50,50,10. The layer has a default anchor point of 0.5,0.5. So it computes its bounds as 0,0,50,10 and its position as 75,55.
By default anchorPoint and position are coinciding.
If we change anchorPoint to 1,0.5 it will change the position and hence center also to be at 100,55 . But instead of moving center/position it moves the frame of label. so if we want 75,55 as the new anchorPoint at 1,1, we will have to shift the frame by -25,0 (75,55 - 100,55). hence frame gets changed from what is given by the console output.
Label frame for 0 label before anchor point shift is {{50, 50}, {50, 10}}
Label frame for 0 label after anchor point shift is {{25, 50}, {50, 10}}
The frame of a UIView is a derived quantity based on the center and bounds properties. If you change the center or bounds, then the frame is adjusted accordingly. Likewise, changing the frame adjusts the center and/or bounds as necessary.
Ok, that's great, except that you're changing the anchorPoint of the layer. When you change the anchorPoint of the layer, that changes the center point of the layer, which changes the center point of the UIView, which then changes the frame. Hence, the label moves.

iOS, setTransform overridden by setFrame? Transform not re-drawing after setFrame is re-run

My transform does not draw after the frame is redrawn with setFrame.
I'm scaling a view when the orientation changes using setFrame. But this view also needs to change position depending on a BOOL argument: On = up in view, off = down off screen. I use setTransform to adjust the position of the view.
First I draw the view by doing a setFrame. This draws the view 100 points just off screen at the bottom. Then I set a transform (-100 on the ty) to bring it up into the view points (as long as the BOOL is set to TRUE, else the transform is 0 and the view is off screen).
[view setFrame:CGRectMake(0, self.view.bounds.size.height, self.view.bounds.size.width, 100)];
[view setTransform:CGAffineTransformMake(1, 0, 0, 1, 0, -100)]
This works fine, as expected, but when change orientation, and this code is re-run (to re-size the view) the transform does not draw, even though it is Logged as being the transform of the view. In other words the view is just off screen, as if the transform.ty was 0.
Log message before re-draw: view.transform.ty -10.000000
Log message after re-draw: view.transform.ty -10.000000
Any help?
A view's frame is a value that is derived from three fundamental values: bounds, center, and transform. The setter for frame tries to do the right thing by reversing the process, but it can't always work correctly when a non-identity transform is set.
The documentation is pretty clear on this point:
If the transform property is not the identity transform, the value of
this property is undefined and therefore should be ignored.
...
If the transform property contains a non-identity transform, the value
of the frame property is undefined and should not be modified. In that
case, you can reposition the view using the center property and adjust
the size using the bounds property instead.
Since your transform only translates the view, I don't see any reason to use it at all. Just change the origin of the view's frame:
[view setFrame:CGRectMake(0, self.view.bounds.size.height - 100, self.view.bounds.size.width, 100)];

Resources