Setting the view's frame as old but its position changed - ios

I built a demo app to check the relationship between layer's anchorpoint, position and frame.
The initial look of the view looks like following:
In the code, I change that red view's anchor point, it will looks like this, which I could understand since change of anchor point will affect that view's frame.
to maintain the view's frame as original one, I used the following code:
We could see from the console's printout the frame has already remained the same.
However the view's final look looks like following, which still changes its position, how could this happen?
All the code looks like this:
Code are as following:
// Reserve original frame
let oldFrame = self.controlledView.frame
// Here I changed the anchorPoint which will cause that view's frame change
self.controlledView.layer.anchorPoint = CGPoint(x: 0, y: 0)
// to avoid the change of frame, set it back
self.controlledView.frame = oldFrame
// From the console's log the frame doesn't change, the red view's final
// location should be the same with the first image. However it is aligned to the right,
// which I could not understand.

From the CALayer Class Reference on Apple Documentation
You specify the value for this property using the unit coordinate space. The default value of this property is (0.5, 0.5), which represents the center of the layer’s bounds rectangle. All geometric manipulations to the view occur about the specified point. For example, applying a rotation transform to a layer with the default anchor point causes the layer to rotate around its center. Changing the anchor point to a different location would cause the layer to rotate around that new point.
From the UIView Class Reference on Apple Documentation
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. Setting this property changes the point specified by the center property and the size in the bounds rectangle accordingly. The coordinates of the frame rectangle are always specified in points
So from my point of view, once the view is inside another view, when you change the frame you are changing it's center and size relative to his superview and not with itself.
To test my theory, i perform a small example
import UIKit
class ViewController: UIViewController {
var insideView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
// Setup Inside view
self.insideView = UIView()
var frame: CGRect = CGRectZero
frame.size.height = 40.0
frame.size.width = 40.0
frame.origin.x = self.view.frame.size.width / 2 - 20.0
frame.origin.y = self.view.frame.size.height / 2 - 20.0
self.insideView.frame = frame
self.insideView.backgroundColor = UIColor.redColor()
self.view.addSubview(self.insideView)
NSLog("One layer -> InsideView size: %#", NSStringFromCGRect(self.insideView.frame))
// Output: 2015-05-22 20:13:11.342 test[42680:12030822] One layer -> InsideView size: {{140, 264}, {40, 40}}
// Setup Another layer
var insideLayer: CALayer = CALayer()
insideLayer.backgroundColor = UIColor.blueColor().CGColor
var insideLayerFrame: CGRect = self.insideView.layer.frame;
insideLayerFrame.origin.x = 0.0
insideLayerFrame.origin.y = 0.0
insideLayer.frame = insideLayerFrame
insideLayer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
self.insideView.layer.addSublayer(insideLayer)
NSLog("Two layers -> InsideView size: %#", NSStringFromCGRect(self.insideView.frame))
// Output: 2015-05-22 20:13:11.342 test[42680:12030822] Two layers -> InsideView size: {{140, 264}, {40, 40}}
}
}
So i leave the layer of the view in it's position and add a new that i can manipulate.
And the result is:
Hope this can help :)

Related

Get rendered bounds of a view in viewDidLoad()

I've been searching for help on this all day but I can't find an answer.
I have a subview in which I am drawing a rectangle, the rectangle is framed by the subview so I need to know the size of the subview as adjusted by autolayout to correctly frame it. I can't find a way of doing this from ViewDidLoad(), so that the rectangle is correctly rendered at start-up. I have tried the following:
Using dayView.setNeedsLayout() followed by dayView.layoutIfNeeded() before I draw the rectangle in viewDidLoad() but a check either side of these statements shows the dayView.bounds unchanged.
Drawing the view from viewDidLayoutSubviews(), which works, but results in my rectangle being drawn 5 times as viewDidLayoutSubviews() is called for every subview (I have 5 of them) that is redrawn (the relevant subview containing the rectangle is redrawn on call 4 of 5) - this seems wasteful of resources, surely there is a better way?
Drawing the view twice within ViewDidLoad(), hoping the first forced draw will cause the view to be resized, so the second draw will have access to the new bounds after the first draw (desperate I know, but it still doesn't work).
I hope someone can help.
func drawGradient(object: UIView, rect: CGRect, slackX: Int, gradWidth: Int, yPos: Int) -> Void {
// the rectangle width and height set to fit within view with x & y border.
let gradientView = UIView(frame: rect)
let gradient = CAGradientLayer()
gradient.frame = gradientView.frame
gradient.colors = getDayGradientLocations().gradientCol
gradient.locations = getDayGradientLocations().gradientLoc
dayView.layer.addSublayer(gradient)
let civilDawn = getTimeAsProportionOfDay(time: tides.civilDawn) * Double(rect.height) + Double(yPos)
let path = UIBezierPath()
path.move(to: CGPoint(x: slackX, y: Int(civilDawn)))
path.addLine(to: CGPoint(x: slackX + Int(rect.width), y: Int(civilDawn)))
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = CGFloat(2)
let civilDusk = getTimeAsProportionOfDay(time: tides.civilDusk) * Double(rect.height) + Double(yPos)
path.move(to: CGPoint(x: slackX, y: Int(civilDusk)))
path.addLine(to: CGPoint(x: slackX + Int(rect.width), y: Int(civilDusk)))
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = CGFloat(2)
object.layer.addSublayer(shapeLayer)
drawLabel(object: object, rect: rect, slackX: slackX, offset: 15, time: tides.civilDawn)
drawLabel(object: object, rect: rect, slackX: slackX, offset: -15, time: tides.civilDusk)
}
A couple of observations:
If you are adjusting a view’s frame in your view controller, the right place to do this is in viewDidLayoutSubviews. Yes, this is called a number of times, but it generally doesn’t have any observable impact on performance.
I wouldn't advise any of those extremely brittle techniques of setNeedsLayout, layoutIfNeeded, or DispatchQueue.main.async inside viewDidLoad. The viewDidLayoutSubviews is the right place if you’re going to do this in the view controller.
Generally, if doing custom subview drawing and layout of subviews, you do this in the layoutSubviews of the view, rather than in any of the view controller methods.
Likewise, if you're doing any manual drawing, you’d put that in the UIView subclass (or any of the relevant CALayer subclasses), not the view controller.
Even better, rather than adjusting frames manually, it’s better to let the auto layout system handle this for you if you can. If you find yourself manually adjusting a frame, there are generally better patterns.
FWIW, you can define a container view’s constraints to be based upon the size (or intrinsic size) of its subviews (and set the content-hugging and compression-resistance of the relevant views). We often think of auto-layout as a top-down engine, but it works both ways.
If you show us your “drawing” code and/or a screen snapshot or two, we can probably offer more concrete counsel.
Got to main thread in viewDidLoad and Perform action will resolve your issue
DispatchQueue.main.async {
// here you can add anything it will have proper frame
}

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

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.

images on iOS becoming narrow and squeezed when I apply rotation and transformation

I am using the following code to rotate and transform an image view:
myImageView.transform = CGAffineTransformMakeRotation(45); // rotation
CGRect frame = myImageView.frame;
frame.origin.x = x_position;
frame.origin.y = y_position;
myImageView.frame = frame; // transformation
tl;dr: The frame is bogus when you have a non-identity transform. Change the center instead.
From the documentation:
Warning If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
When you are setting the transform on the first line and then reading the frame after it is undefined what you actually get back.
// Setting a non-identity transform (1)
myImageView.transform = CGAffineTransformMakeRotation(45);
CGRect frame = myImageView.frame; // At this point the frame is undefined (2)
// You are modifying something which is undefined from now on ...
Also, not only should you not read the frame because it is undefined, you should also not set it.
if the transform property contains a non-identity transform, the value of the frame property is undefined and should not be modified.
The solution to that problem comes in the next sentence of the documentation
In that case, you can reposition the view using the center property and adjust the size using the bounds property instead.
Since you are only changing the position I would suggest that you don't touch the frame and instead read the center of the image view and set it to it's new value. The center is not affected by the transform in the same way as the frame.
CGPoint center = myImageView.center;
center.x = x_center_position; // Note that the `center` is not the same as the frame origin.
center.y = y_center_position; // Note that the `center` is not the same as the frame origin.
myImageView.center = center;
Try to remove autoresizing Mask of ImageView
imageView.autoresizingMask = UIViewAutoresizingNone;
This may solve your problem

How to set a UIView's origin reference?

I am creating a UIImageView and adding it in a loop to my view, I set the initial frame to 0,0,1,47 and each passage of the loop I change the center of the image view to space them out.
I am always using 0 as the origin.y
The problem is the origin reference is in the centre of the image view, assuming we was in interface builder, this is equivalent to the image below.
How can I change the reference point in code ?
After reading these answers and your comments I'm not really sure what is your point.
With UIView you can set position by 2 ways:
center – It definitely says it is the center.
frame.origin – Top left corner, can't be set directly.
If you want the bottom left corner to be at x=300, y=300 you can just do this:
UIView *view = ...
CGRect frame = view.frame;
frame.origin.x = 300 - frame.size.width;
frame.origin.y = 300 - frame.size.height;
view.frame = frame;
But if you go one level deeper to magical world of CALayers (don' forget to import QuartzCore), you are more powerful.
CALayer has these:
position – You see, it don't explicitely says 'center', so it may not be center!
anchorPoint – CGPoint with values in range 0..1 (including) that specifies point inside the view. Default is x=0.5, y=0.5 which means 'center' (and -[UIView center] assumes this value). You may set it to any other value and the position property will be applied to that point.
Example time:
You have a view with size 100x100
view.layer.anchorPoint = CGPointMake(1, 1);
view.layer.position = CGPointMake(300, 300);
Top left corner of the view is at x=200, y=200 and its bottom right corner is at x=300, y=300.
Note: When you rotate the layer/view it will be rotated around the anchorPoint, that is the center by default.
Bu since you just ask HOW to do specific thing and not WHAT you want to achieve, I can't help you any further now.
The object's frame includes its position in its superview. You can change it with something like:
CGRect frame = self.imageView.frame;
frame.origin.y = 0.0f;
self.imageView.frame = frame;
If I am understanding you correctly, you need to set the frame of the image view you are interested in moving. This can be done in the simple case like this:
_theImageView.frame = CGRectMake(x, y, width, height);
Obviously you need to set x, y, width, and height yourself. Please also be aware that a view's frame is in reference to its parent view. So, if you have a view that is in the top left corner (x = 0, y = 0), and is 320 points wide and 400 points tall, and you set the frame of the image view to be (10, 50, 100, 50) and then add it as a subview of the previous view, it will sit at x = 10, y = 50 of the parent view's coordinate space, even though the bounds of the image view are x = 0, y = 0. Bounds are in reference to the view itself, frame is in reference to the parent.
So, in your scenario, your code might look something like the following:
CGRect currentFrame = _theImageView.frame;
currentFrame.origin.x = 0;
currentFrame.origin.y = 0;
_theImageView.frame = currentFrame;
[_parentView addSubview:_theImageView];
Alternatively, you can say:
CGRect currentFrame = _theImageView.frame;
_theImageView.frame = CGRectMake(0, 0, currentFrame.size.width, currentFrame.size.height);
[_parentView addSubview:_theImageView];
Either approach will set the image view to the top left of the parent you add it to.
I thought I would take a cut at this in Swift.
If one would like to set a views position on the screen by specifying the coordinates to an origin point in X and Y for that view, with a little math, we can figure out where the center of the view needs to be in order for the origin of the frame to be located as desired.
This extension uses the frame of the view to get the width and height.
The equation to calculate the new center is almost trivial. See the below extension :
extension CGRect {
// Created 12/16/2020 by Michael Kucinski for anyone to reuse as desired
func getCenterWhichPlacesFrameOriginAtSpecified_X_and_Y_Coordinates(x_Position: CGFloat, y_Position: CGFloat) -> CGPoint
{
// self is the CGRect
let widthDividedBy2 = self.width / 2
let heightDividedBy2 = self.height / 2
// Calculate where the center needs to be to place the origin at the specified x and y position
let desiredCenter_X = x_Position + widthDividedBy2
let desiredCenter_Y = y_Position + heightDividedBy2
let calculatedCenter : CGPoint = CGPoint(x: desiredCenter_X, y: desiredCenter_Y)
return calculatedCenter // Using this point as the center will place the origin at the specified X and Y coordinates
}
}
Usage as shown below to place the origin in the upper left corner area, 25 pixels in :
// Set the origin for this object at the values specified
maskChoosingSlider.center = maskChoosingSlider.frame.getCenterWhichPlacesFrameOriginAtSpecified_X_and_Y_Coordinates(x_Position: 25, y_Position: 25)
If you want to pass a CGPoint into the extension instead of X and Y coordinates, that's an easy change you can make on your own.

Resources