Detecting rotated width/height of UIView after orientation change - ios

I would like to be able to inspect a UIView instance that may or may not have been rotated from its original orientation (by the user rotating the device for instance) and determine what the "true" width and height are. "true" here meaning, if a view on a portrait-oriented iPad was 768x1024 before rotation, after being turned sideways I would calculate that the new width was 1024 and the new height was 768.
It appears that if I apply the view's transform to its frame property like this:
CGRect rotated = CGRectApplyAffineTransform([myview frame], [myview transform);
I get the desired result. Apple's documentation however states that UIView::frame is undefined if the transform for the view is not the identity transform, so maybe it's not a good idea to rely on this calculation?

view.bounds will return the rect you want
view.frame will return a rect with the transfrom applied to the bounds along with position.

Well due to lack of input, I'm going to go with my solution of using:
CGRect rotated = CGRectApplyAffineTransform([myview frame], [myview transform);
To get the properly oriented bounding. If somebody has another solution or can confirm this is safe I will award the answer to them instead.

Related

Why do layer transforms affect a UIView's frame?

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.

Moving a Rotated UIButton

I have a problem. I'm working on making a game. As part of my game I need images to be rotated and then moved in the direction of the rotated angle inside a game loop (using an NSTimer). In essence I'm trying to create the effect of launching a projectile. The code works fine when moving in perpendicular directions such as 0, 90, 180, 270, and 360 degrees, but any other angle and the image starts to glitch out. The object on the screen maintains its correct bounds and contents, but the actual displayed image disappears. Does anybody know what the problem is or someway I could get around it? If needed, I can make and post a video of my problem so you can see what I'm talking about.
Here is a sample of the code I'm using. The "background" variable is just a UIImageView:
angle = 60;
background.transform = CGAffineTransformRotate(object.transform, angle*M_PI/180); //converts degrees to radians and rotates the image
background.frame = CGRectMake( background.frame.origin.x + cos(angle*m_PI/180)*32; background.frame.origin.y -sin(angle*M_PI/180)*32, background.frame.size.width, background.frame.size.height); //moves the image in the direction of the angle
For starters, there is a semicolon after the x origin in your CGRect instead of a comma. Was that just a typo?
The UIView documentation for frame states:
Warning: If the transform property is not the identity transform, the
value of this property is undefined and therefore should be ignored.
Changes to this property can be animated. However, 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.
So there you have it, you should not be trying to change the frame when setting a custom transform. You are only trying to adjust the position of the view anyway so just modify your code to adjust center instead of the origin coordinates.
To change the size, you can use the bounds.
CGRect bounds = myView.bounds;
bounds.size.width = whatever;
bounds.size.height = whatever;
myView.bounds = bounds;

Animating the bounds property of a UIView

According to the Apple documentation available here the bounds property of a UIView can be animated.
In order to programmatically rotate one of my views (in this case from portrait to landscape), I implemented the following code:
float w = controlsView.bounds.size.width;
float h = controlsView.bounds.size.height;
CGAffineTransform T = CGAffineTransformMakeRotation(M_PI/2);
UIViewContentMode oldContentMode = controlsView.contentMode;
controlsView.contentMode = UIViewContentModeRedraw;
[UIView animateWithDuration:1.0 animations:^{
controlsView.bounds = CGRectMake(0, 0, h, w);
controlsView.transform = T;
}];
controlsView.contentMode = oldContentMode;
I added the two instructions for contentMode because I have read somewhere on StackOverflow that contentMode must be set to that value in order for a UIView to redraw the content while the bounds property is modified (however, in this case it does not affect the behaviour in any way).
The problem of that code is that UIView is not resized with an animation but all at once.
Executing that code, the UIView is first resized (without animation) then rotated with an animation.
How can I animate the resizing?
-- UPDATE --
I tried to combine a scaling and rotating transformation. The UIView rotates and scales in an animation, but at the end its bounds property does not change: even if it has been rotated to a landscape orientation (which should be 568x320 on my iPhone), its width and height stay at the portrait values (320x568). Indeed, the UI controls on the controlsView are deformed (wrong aspect ratio) and appears stretched.
This worked for me using UIViewAnimationOptions.LayoutSubviews (Swift):
UIView.animateWithDuration(1.0,delay:0.0,
options:UIViewAnimationOptions.LayoutSubviews,
animations:{ () -> Void in
myView.transform = myRotationTransform
myView.bounds = myBounds
}, completion: nil)
(no explicit scaling nor changing contentMode)
The problem is that when you set a transform property, all other transforms (bounds) are overwritten. To fix it you have to create one transform that combines scaling and rotating.
Example:
CGAffineTransform scaleTransform = CGAffineTransformMakeScale(0.5, 0.5);
[UIView animateWithDuration:1.0 animations:^
{
controlsView.transform = CGAffineTransformRotate(scaleTransform, M_PI/2);
}];
You will need to calculate the scaling X and Y values based on the size you want to have at the end of the animations. You might also want to change the anchorPoint property to set a center point used for scaling (controlsView.layer.anchorPoint).
Animating your bounds doesn't change its value. It reverts to its original value once the animation ends.
Change the value of the bounds to the final value once the animation is over. I'll solve the trouble.

why self.view.frame and self.view.bounds are different when the devices rotate?

I want to do some layout change when the devices rotate. So I implement - (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration method to do the work. But I realize when this method is called the self.view.frame and self.view.bounds are different. The self.view.bounds.size is correct and the self.view.frame.size seems still not rotate.
For example, I created an empty singleView Project and implemented the method like follows:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
NSLog(#"willAnimateRotationToInterfaceOrientation");
NSLog(#"self.view.bounds.size width:%f height:%f ",self.view.bounds.size.width,self.view.bounds.size.height);
NSLog(#"self.view.frame.size width:%f height:%f",self.view.frame.size.width,self.view.frame.size.height);
}
when the device rotates from portrait to landscape. the output is as follows:
2013-06-11 11:57:46.959 viewDemo[95658:707] willAnimateRotationToInterfaceOrientation
2013-06-11 11:57:46.961 viewDemo[95658:707] self.view.bounds.size width:1024.000000 height:748.000000
2013-06-11 11:57:46.961 viewDemo[95658:707] self.view.frame.size width:748.000000 height:1024.000000
I wonder why these sizes are different? They shouldn't be always the same? And when to choose which one to use?
Any help will be appreciated.
Look at the answer by "tc.", which is accepted as of now. Copying few lines from that answer to here.
So, the "frame" is relative to the parent view, which in this case is the UIWindow. When you rotate device, the window doesn't rotate; the view controller's view does. From the window's perspective, everything is effectively in portrait mode. This is why even after device is rotated to landscape, self.view.frame will not change.
You should use self.view.bounds to perform any calculation as it gives you the correct values independent of your device orientation.
I wonder why these values are different? They shouldn't be always the same?
The bounds of an UIView is the rectangle, expressed as a location (x, y) and size (width, height) relative to its own coordinate system (0,0).
The frame of an UIView is the rectangle, expressed as a location (x, y) and size (width, height) relative to the superview it is contained within.
In the case of the bounds, the x and y coordinates are at 0,0 as these coordinates are relative to the view itself. However, the frame x and y coordinates are relative to the position of the view within the parent view
And when to choose which one to use?
Hopefully this helps clarify the circumstances where each property might get used.
UIView's frame, bounds, center, origin, when to use what?
please note that frame.size is not equal to bounds.size when the simulator is rotated
refer to this one UIView frame, bounds and center
Just use method did(The size is new) not will(The size is same as was in parent controller)
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
[halfView setFrame:CGRectMake(0, 0, self.view.bounds.size.width, 120)];
NSLog(#"willAnimateRotationToInterfaceOrientation");
NSLog(#"self.view.bounds.size width:%f height:%f ",self.view.bounds.size.width,self.view.bounds.size.height);
NSLog(#"self.view.frame.size width:%f height:%f",self.view.frame.size.width,self.view.frame.size.height);
}

UIKeyboard's Frame is 20 pixels off

I need someone to explain this to me as it is not making any sense.
When getting the UIKeyboards frame from the userInfo using UIKeyboardFrameEndUserInfoKey and doing the math so that a view would appear to be stacked on top of the keyboard, I need to make a difference of 20 pixels.
The math:
CGRect frame = view.frame;
CGPoint origin = frame.origin;
origin.x = kbFrame.origin.x;
origin.y = kbFrame.origin.y - view.frame.size.height - 20;
frame.origin = origin;
view.frame = frame;
I thought it must be the status bar, but here's the kicker, I'm developing on a retina display and so the status bar is 40 pixels in height not 20.
I then added a the conversion from view to view
CGRect kbFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIWindow * window = [UIApplication sharedApplication].windows[0];
kbFrame = [self.view convertRect:kbFrame fromView:window];
and that seems to fixed of it; that is I can remove the 20 pixels difference.
What really throws me is when using UIKeyboardFrameBeginUserInfoKey, the keyboard's frame is at the bottom of the screen, which is correct, but the end frame causes me to encode a 20 pixel difference. When I add the conversion code in, it puts the keyboard 20 pixels up and so gets rid of the difference. What the hell is going on?
The thing you are describing is the natural and wanted behaviour.
The thing is that the keyboard coordinate are in the Window Coordinate system.
Your view is probably not in Window Coordinate system. So you always need to do a conversion between coordinate system to be able to use it correctly.
What really throws me is when using UIKeyboardFrameBeginUserInfoKey, the keyboard's frame is at the bottom of the screen, which is correct,
I would disagree, you are probably 20 points lower than what you think you are (if you are setting it in your View coordinate system), but since that is off the screen you don't notice the offset.
Every View have it's own coordinate system that is inside it's bounds. And the frame of a view is expressed in it's parent coordinate system. That can also lead to some confusion if we don't understand the why and necessity of this difference.
I hope this will help you.
NOTE on Retina Display and Measurement :
On iOS you never deal with Pixel in your code (only when preparing your assets) you always deal with Point. So the Status bar is always 20 points, on Retina or not.

Resources