calculate new size and location on a CGRect - ios

I have a CGRect (let's call it A1) with know size and location that has a parent A of know size. A1 has all sort of CGAffineTransform applied to it.
I need to find the location and size of a second rectangle relative to a parent B of known (but different dimensions) that would look proportional to A1 in relation to A
Basically a "zoom" effect. What would A1 dimensions and position be to look similar when placed in B . Kind of a "scale the CGAffineTransform" ?
visual guide:
From:
To:

Assuming you are transforming the rect like this:
scale it
move it
rotate it
First, you want to calculate the "scaleFactor" between the two sizes. So...
if rect A was 100 x 100
and rect B was 200 x 200
The scaleFactor would be 200 / 100 == 2 ... that is, B is 2x bigger.
For your values:
rect A is 375 x 375
rect B is 1080 x 1080
the scaleFactor is 1080 / 375 == 2.88
So, let's go with values that make the math easy (and obvious):
rect A is 200 x 200
rect A1 origin is x: 30 y: 20
rect A1 size is 50 x 25
rect B is 600 x 600
scaleFactor is 600 / 200 == 3, so
rect B1 origin is x: (30 * 3) y: (20 * 3)
rect B1 size is (50 * 3) x (25 * 3)
If that's all we've done so far, we would get this result:
Next, we need to move (translate) it, and scale the translation. If the original translation is:
CGAffineTransform(translationX: 30, y: 100)
the new translation is:
CGAffineTransform(translationX: 30 * scaleFactor, y: 100 * scaleFactor)
We don't need to scale the rotation, because, well, it's the same rotation.
So, after setting the original frame based on our origin and size calculations, then applying the same scale transform, plus the scaled translation transform, plus the same rotation transform, we get:
Here's example code I used to produce that:
class ViewController: UIViewController {
let viewA: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
let viewARedView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let viewABlueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
let viewB: UIView = {
let v = UIView()
v.backgroundColor = .systemYellow
return v
}()
let viewBRedView: UIView = {
let v = UIView()
v.backgroundColor = .systemRed
return v
}()
let viewBBlueView: UIView = {
let v = UIView()
v.backgroundColor = .systemBlue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.setNavigationBarHidden(true, animated: false)
view.addSubview(viewA)
view.addSubview(viewB)
viewA.frame = CGRect(x: 20, y: 60, width: 200, height: 200)
viewB.frame = CGRect(x: 20, y: viewA.frame.maxY + 20, width: 600, height: 600)
viewA.addSubview(viewARedView)
viewA.addSubview(viewABlueView)
let origRect: CGRect = CGRect(x: 30, y: 20, width: 50, height: 25)
// set frames of red and blue rect to the same rectangles
// so blue is on top of red
// we're going to transform the blue rect
viewARedView.frame = origRect
viewABlueView.frame = origRect
// scale values
let scX: CGFloat = 2.25
let scY: CGFloat = 1.75
// translate values
let trX: CGFloat = 30.0
let trY: CGFloat = 100.0
// rotation value
let rot: CGFloat = .pi * -0.15
var tx: CGAffineTransform = .identity
// scale it
tx = tx.concatenating(CGAffineTransform(scaleX: scX, y: scY))
// move it
tx = tx.concatenating(CGAffineTransform(translationX: trX, y: trY))
// rotate it
tx = tx.concatenating(CGAffineTransform(rotationAngle: rot))
// apply the transform
viewABlueView.transform = tx
viewB.addSubview(viewBRedView)
viewB.addSubview(viewBBlueView)
// get the scale factor...
// in this case,
// viewA width is 200
// viewB width is 700
// so viewB is 3.5 times bigger (700 / 200)
let scaleFactor: CGFloat = viewB.frame.width / viewA.frame.width
// make the original rect origin and size
// "scaleFactor bigger"
let newRect: CGRect = CGRect(x: origRect.origin.x * scaleFactor,
y: origRect.origin.y * scaleFactor,
width: origRect.width * scaleFactor,
height: origRect.height * scaleFactor)
// set frames of red and blue rect to the same rectangles
// so blue is on top of red
// we're going to transform the blue rect
viewBRedView.frame = newRect
viewBBlueView.frame = newRect
// scale the translation values
let trXb: CGFloat = trX * scaleFactor
let trYb: CGFloat = trY * scaleFactor
var txb: CGAffineTransform = .identity
// we've already changed the initial size of the rectangle,
// so use same scaling values
txb = txb.concatenating(CGAffineTransform(scaleX: scX, y: scY))
// we need to use scaled translation values
txb = txb.concatenating(CGAffineTransform(translationX: trXb, y: trYb))
// rotation doesn't change based on scale
txb = txb.concatenating(CGAffineTransform(rotationAngle: rot))
// apply the transform
viewBBlueView.transform = txb
}
}

Related

How to get random coordinates inside a swift UIView?

I'm creating an animation function to generate imageViews randomly inside a uiView. Yet it is always returning 0,0 Please Help!
private func coinAnimation(image: UIImage) {
let imageView = UIImageView(image: image)
imageView.frame = self.view.convert(self.coinsView.frame, from: self.coinsView.superview!)
imageView.contentMode = UIView.ContentMode.scaleAspectFit
imageView.backgroundColor = .red
let frame = coinsView.frame
let x = randomInRange(lo: 0, hi: Int(frame.size.width - imageView.bounds.size.width))
let y = randomInRange(lo: 0, hi: Int(frame.size.height - imageView.bounds.size.height))
let position = CGPoint(x: x, y: y)
UIView.animate(withDuration: 1.0, delay: 0.0, options: .curveEaseInOut, animations: {
imageView.center = position
self.coinsView.addSubview(imageView)
}, completion: nil)
}
private func randomInRange(lo: Int, hi : Int) -> Int {
return lo + Int(arc4random_uniform(UInt32(hi - lo + 1)))
}
Your issue is with the line below
imageView.frame = self.view.convert(self.coinsView.frame, from: self.coinsView.superview!)
This line is setting the same frame of coinsView to imageView. And when you are trying to get random x and y points in the below code it will always return 0. because frame.size.width and imageView.bounds.size.width are same and same goes to height.
let x = randomInRange(lo: 0, hi: Int(frame.size.width - imageView.bounds.size.width))
let y = randomInRange(lo: 0, hi: Int(frame.size.height - imageView.bounds.size.height))
For better understanding add below line to your code to set imageView Frame, you need to set static frame to your image view initially.
imageView.frame = CGRect(x: 0, y: 0, width: 30, height: 30)
Make sure width and height should be smaller than the width and height of coinsView's width & height

How to get x and y position of UIImage in UIImageView?

I want to get original x and y position of UIImage when we set it in UIImageView with scaleAspectFill.
As we know in scaleAspectFill, some of the portion is clipped. So as per my requirement I want to get x and y value (it may be - value I don't know.).
Here is the original image from gallery
Now I am setting this above image to my app view.
So as above situation, I want to get it's hidden x, y position of image which are clipped.
Can any one tell how to get it?
Use following extension
extension UIImageView {
var imageRect: CGRect {
guard let imageSize = self.image?.size else { return self.frame }
let scale = UIScreen.main.scale
let imageWidth = (imageSize.width / scale).rounded()
let frameWidth = self.frame.width.rounded()
let imageHeight = (imageSize.height / scale).rounded()
let frameHeight = self.frame.height.rounded()
let ratio = max(frameWidth / imageWidth, frameHeight / imageHeight)
let newSize = CGSize(width: imageWidth * ratio, height: imageHeight * ratio)
let newOrigin = CGPoint(x: self.center.x - (newSize.width / 2), y: self.center.y - (newSize.height / 2))
return CGRect(origin: newOrigin, size: newSize)
}
}
Usage
let rect = imageView.imageRect
print(rect)
UI Test
let testView = UIView(frame: rect)
testView.backgroundColor = UIColor.red.withAlphaComponent(0.5)
imageView.superview?.addSubview(testView)
Use below extension to find out accurate details of Image in ImageView.
extension UIImageView {
var contentRect: CGRect {
guard let image = image else { return bounds }
guard contentMode == .scaleAspectFit else { return bounds }
guard image.size.width > 0 && image.size.height > 0 else { return bounds }
let scale: CGFloat
if image.size.width > image.size.height {
scale = bounds.width / image.size.width
} else {
scale = bounds.height / image.size.height
}
let size = CGSize(width: image.size.width * scale, height: image.size.height * scale)
let x = (bounds.width - size.width) / 2.0
let y = (bounds.height - size.height) / 2.0
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
}
How to test
let rect = imgTest.contentRect
print("Image rect:", rect)
Reference: https://www.hackingwithswift.com/example-code/uikit/how-to-find-an-aspect-fit-images-size-inside-an-image-view
If you want to show image like it shows in gallery then you can use contraints
"H:|[v0]|" and "V:|[v0]|" and in imageview use .aspectFit
And if you want the image size you can use imageView.image!.size and calculate the amount of image which is getting cut. In aspectFill the width is matched to screenwidth and accordingly the height gets increased. So I guess you can find how how much amount of image is getting cut.
Try this Library ImageCoordinateSpace
I am not sure if it works for you or not, but it has a feature to convert CGPoint from image coordinates to any view coordinates and vice versa.

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:
create a CGRect of the frame that I want
rotate that rect 90 degrees anticlockwise
set that rect as the frame of the label
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.
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)
}
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)

How to rotate a CAShapeLayer

I am trying to rotate a CAShapeLayer with respect to a particular anchor point. But when i apply
firstLayer.transform = CATransform3DMakeRotation(CGFloat(M_2_PI), 0, 0, 0)
nothing happens.
i am making a custom UIButton , in which i am adding a layer
import UIKit
#IBDesignable
class CustomButtonTwo: UIButton {
var context = UIGraphicsGetCurrentContext()
#IBInspectable var Thickness : CGFloat = 2
let firstLayer = CAShapeLayer()
var width = CGFloat()
var height = CGFloat()
override func awakeFromNib() {
super.awakeFromNib()
width = self.frame.width
height = self.frame.height
print("\(width) : \(height)")
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let afirstStartPoint = CGPointMake(width * 0.1, (height - 3 * Thickness) / 6)
let bfirstStartPoint = CGPointMake(width * 0.1, (height - 3 * Thickness) / 6 + Thickness)
let afirstMiddlePoint = CGPointMake(width * 0.5, (height - 3 * Thickness) / 6 )
let bfirstMiddlePoint = CGPointMake(width * 0.5, (height - 3 * Thickness) / 6 + Thickness)
print(afirstMiddlePoint)
print(afirstStartPoint)
print(bfirstMiddlePoint)
print(bfirstStartPoint)
let firstPath = UIBezierPath()
firstPath.moveToPoint(afirstStartPoint)
firstPath.addLineToPoint(afirstMiddlePoint)
firstPath.addLineToPoint(bfirstMiddlePoint)
firstPath.addLineToPoint(bfirstStartPoint)
firstPath.addLineToPoint(afirstStartPoint)
firstPath.closePath()
firstLayer.frame = self.frame
UIColor.greenColor().setFill()
firstPath.fill()
firstLayer.path = firstPath.CGPath
firstLayer.anchorPoint = afirstStartPoint
firstLayer.transform = CATransform3DMakeRotation(CGFloat(M_2_PI), 0, 0, 1)
layer.addSublayer(firstLayer)
}
}
i want to make a line with a particular thickness and rotate it along a particular point (while animating).
any help appreciated!
simulator Screenshot
Rotating by 2_PI (360) will mean layer will end up back at its original place (0 rotation). Try PI_2 (90) or PI (180) or another angle. Also you need to specify the axis of rotation.
This call rotates by 90 degrees around z-axis:
CATransform3DMakeRotation(CGFloat(M_PI_2), 0, 0, 1.0)

How can I increase the size of a CGRect by a certain percent value?

How can I increase the size of a CGRect by a certain percent value? Should I use some form of CGRectInset to do it?
Example:
Assume I have a CGRect: {10, 10, 110, 110}
I want to increase its size (retaining the same center point) by 20% to:
{0, 0, 120, 120}
You can use CGRectInset if you like:
double pct = 0.2;
CGRect newRect = CGRectInset(oldRect, -CGRectGetWidth(oldRect)*pct/2, -CGRectGetHeight(oldRect)*pct/2);
To decrease the size, remove the -s.
Side note: A CGRect that is 20% bigger than {10, 10, 100, 100} is {0, 0, 120, 120}.
Edit: If the intention is to increase by area, then this'll do it (even for rectangles that aren't square):
CGFloat width = CGRectGetWidth(oldRect);
CGFloat height = CGRectGetHeight(oldRect);
double pct = 1.2; // 20% increase
double newWidth = sqrt(width * width * pct);
double newHeight = sqrt(height * height * pct);
CGRect newRect = CGRectInset(oldRect, (width-newWidth)/2, (height-newHeight)/2);
In Swift:
func increaseRect(rect: CGRect, byPercentage percentage: CGFloat) -> CGRect {
let startWidth = CGRectGetWidth(rect)
let startHeight = CGRectGetHeight(rect)
let adjustmentWidth = (startWidth * percentage) / 2.0
let adjustmentHeight = (startHeight * percentage) / 2.0
return CGRectInset(rect, -adjustmentWidth, -adjustmentHeight)
}
let rect = CGRectMake(0, 0, 10, 10)
let adjusted = increaseRect(rect, byPercentage: 0.1)
// -0.5, -0.5, 11, 11
In ObjC:
- (CGRect)increaseRect:(CGRect)rect byPercentage:(CGFloat)percentage
{
CGFloat startWidth = CGRectGetWidth(rect);
CGFloat startHeight = CGRectGetHeight(rect);
CGFloat adjustmentWidth = (startWidth * percentage) / 2.0;
CGFloat adjustmentHeight = (startHeight * percentage) / 2.0;
return CGRectInset(rect, -adjustmentWidth, -adjustmentHeight);
}
CGRect rect = CGRectMake(0,0,10,10);
CGRect adjusted = [self increaseRect:rect byPercentage:0.1];
// -0.5, -0.5, 11, 11
Sure, using CGRectInset works:
CGRect someRect = CGRectMake(10, 10, 100, 100);
someRect = CGRectInset(someRect, someRect.size.width * -0.2, someRect.size.height * -0.2);
Swift 4 extension inspired by several of the answers here with simplified calculations:
extension CGRect {
func scaleLinear(amount: Double) -> CGRect {
guard amount != 1.0, amount > 0.0 else { return self }
let ratio = ((1.0 - amount) / 2.0).cgFloat
return insetBy(dx: width * ratio, dy: height * ratio)
}
func scaleArea(amount: Double) -> CGRect {
return scaleLinear(percent: sqrt(amount))
}
func scaleLinear(percent: Double) -> CGRect {
return scaleLinear(amount: percent / 100)
}
func scaleArea(percent: Double) -> CGRect {
return scaleArea(amount: percent / 100)
}
}
Usage is simply:
rect.scaleLinear(percent: 120.0) OR (amount: 1.2)
rect.scaleArea(percent: 120.0) OR (amount: 1.2)
If you are interested in trying my testing methods:
/// Testing
extension CGRect {
var area: CGFloat { return width * height }
var center: CGPoint { return CGPoint(x: origin.x + width/2, y: origin.y + height/2)
}
func compare(_ r: CGRect) {
let centered = center.x == r.center.x && center.y == r.center.y
print("linear = \(r.width / width), area = \(r.area / area) centered \(centered)")
}
static func ScaleTest() {
let rect = CGRect(x: 17, y: 24, width: 200, height: 100)
let percent = 122.6
rect.compare(rect.scaleLinear(percent: percent))
rect.compare(rect.scaleArea(percent: percent))
}
}
I'm using CGRect > insetBy in my Swift code
https://developer.apple.com/documentation/coregraphics/cgrect/1454218-insetby
With this, your percent value will be the scaleX as my example.
let dx = rectWidth*scaleX
let dy = rectHeight*scaleX
let rectangle = CGRect(x: rectX,
y: rectY,
width: rectWidth,
height: rectHeight).insetBy(dx: -dx, dy: -dy)
use positive value to scale down
use negative value to scale up
Using Swift you can retain the center point (relative to the source Rect) and increase/decrease the size as follows using an extension:
extension CGRect {
func centerAndAdjustPercentage(percentage p: CGFloat) -> CGRect {
let x = self.origin.x
let y = self.origin.y
let w = self.width
let h = self.height
let newW = w * p
let newH = h * p
let newX = (w - newW) / 2
let newY = (h - newH) / 2
return CGRect(x: newX, y: newY, width: newW, height: newH)
}
}
let newRect = oldRect.centerAndAdjustPercentage(percentage: 0.25)
let scale = percent / 100
let newRect = oldRect.applying(CGAffineTransform(scaleX: scale, y: scale))
Beware that this approach also will change x and y of rectangle.

Resources