How to scale view from top right corner - ios

I'm changing anchor point by methods in this topic:
Changing my CALayer's anchorPoint moves the view but it's not working for me
What I see
The view is misplaced
What I Expect
The grey rectangle must expand from 1.0 in x and 0.0 in y without misplacement
My code:
private func setAnchorPoint(anchorPoint: CGPoint, view: UIView) {
let oldOrigin = view.frame.origin
view.layer.anchorPoint = anchorPoint
let newOrigin = view.frame.origin
let translation = CGPoint(x: newOrigin.x - oldOrigin.x, y: newOrigin.y - oldOrigin.y)
view.center = CGPoint(x: view.center.x - translation.x, y: view.center.y - translation.y)
}
setAnchorPoint(anchorPoint: CGPoint(x: 1.0, y: 0.0), view: self.actionLayer)
What I've tried
Recording in a variable the frame before changing the anchor point and setting again after, same for layer position or view center. Not working.

Related

Resizing UIView using CGAffineTransform scale doesn't update frame

I'm trying to resize a UIView (Parent) with a few subviews in it using CGAffineTransform scale.
I'm resizing the parent by dragging it from one corner using pan gesture.
The resizing works as expected but if I try to resize it again, it jumps back to the initial frame. It's like it never knew it was resized.
These are the steps I am doing so far:
1.- Just when the pan gesture begins I get the initial frame and the touch location in superview:
if gesture.state == .began {
//We get all initial values from the first touch
initialFrame = self.frame;
touchStart = gesture.location(in: superview)
}
2.- Then I go to the handle I'm dragging (Top right in this case), set the anchor point, calculate deltas (Initial touch - gesture distance traveled), calculate new frame, scales, and apply transform.
case topRight:
if gesture.state == .began {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0, y: 1))
}
let deltaX = -1 * (touchStart.x - gesture.location(in: superview).x)
let deltaY = 1 * (touchStart.y - gesture.location(in: superview).y)
let newWidth = initialFrame.width + deltaX;
let newHeight = initialFrame.height + deltaY;
let scaleX:CGFloat = newWidth / initialFrame.width;
let scaleY:CGFloat = newHeight / initialFrame.height;
self.transform = CGAffineTransform.identity.scaledBy(x: scaleX, y: scaleY)
3.- Finally I reset the anchor point to the middle of the UIView.
if gesture.state == .ended {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
}
I attached a gif where you can see the UIView is resized from the top right handle. When I try to resize it again, it jumps back to the initial frame. (It seems that the video is restarted, but this is the jump)
what am I missing? do I need to update something else?
Thank you all!
So the answer is actually quite simple, this following line of code was incorrect:
self.transform = CGAffineTransform.identity.scaledBy(x: scaleX, y: scaleY)
Here I am applying the scaling to the identity transform which is always the original size.
Just by changing this to:
self.transform = self.initialTransform.scaledBy(x: scaleX, y: scaleY)
Now I am applying the scaling to the current transform which is always saved when the gesture begins.
#objc func handleUserPan(gesture:UIPanGestureRecognizer) {
if gesture.state == .began {
self.initialTransform = self.transform;
self.touchStart = gesture.location(in: resizableSelf.superview);
}
switch gesture.view! {
case self.topRight: //This is just the circle view
if gesture.state == .began {
self..setAnchorPoint(anchorPoint: CGPoint(x: 1, y: 1))
}
let deltaX = 1 * (self.touchStart.x - gesture.location(in: superview).x)
let deltaY = 1 * (self.touchStart.y - gesture.location(in: superview).y)
let newWidth = self.initialFrame.width + deltaX;
let newHeight = self.initialFrame.height + deltaY;
let scaleX:CGFloat = newWidth / self.initialFrame.width;
let scaleY:CGFloat = newHeight / self.initialFrame.height;
self.transform = self.initialTransform.scaledBy(x: scaleX, y: scaleY)
}
default:()
}
gesture.setTranslation(CGPoint.zero, in: self)
self.updateDragHandles()
if gesture.state == .ended {
self.setAnchorPoint(anchorPoint: CGPoint(x: 0.5, y: 0.5))
}
}
Hopefully this can be helpful for those who would like to scale a UIView similar to like Photoshop does with it's layers. It even works if the subviews were rotated.
Keep in mind that once the view is transformed, you cannot set the frame value anymore. That's why by having all views inside a superView is more convenient so it takes care of all resizing.
The goal was to replicate the same behavior that Photoshop does when resizing layers.

CAShapeLayer path animation shrinking before completing animation

I am trying to animate a rotation of a layer's CGpath.
The rotation works perfectly, but for some reason, the path seems to shrink, then get bigger again. I just want it to show a rotating animation. I don't want it to shrink
here is a video.
here is the code
class ViewController: UIViewController {
#IBOutlet weak var overlayView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let maskLayer = CAShapeLayer()
let path = UIBezierPath(rect: view.bounds)
let mask = UIBezierPath.drawSquare(width: 100, center: view.center)
// Makes it so we can see through the center of the view
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
path.append(mask)
maskLayer.path = path.cgPath
overlayView.layer.mask = maskLayer
}
#IBAction func rotateView(_ sender: Any) {
let maskPath = UIBezierPath.drawSquare(width: 100, center: overlayView.center)
let path = UIBezierPath(rect: overlayView.frame)
let bounds: CGRect = maskPath.cgPath.boundingBox
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radians = 90 / 180.0 * .pi
var transform: CGAffineTransform = .identity
transform = transform.translatedBy(x: center.x, y: center.y)
transform = transform.rotated(by: radians)
transform = transform.translatedBy(x: -center.x, y: -center.y)
maskPath.apply(transform)
path.append(maskPath)
// create new animation
let anim = CABasicAnimation(keyPath: "path")
anim.toValue = path.cgPath
(overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "path")
}
}
extension UIBezierPath{
static func drawSquare(width: CGFloat, center: CGPoint) -> UIBezierPath {
let rect = CGRect(x: center.x - width/2, y: center.y - width/2, width: width, height: width)
let path = UIBezierPath()
path.move(to: rect.origin)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.close()
return path
}
}
Your "cutout box" is shrinking and growing because you're using path animation.
When you animate a path from one set of points to another, each point will take a straight line from its starting point to its ending point.
Couple ways to visualize this...
First, we'll add an "outline" frame to the cutout mask box:
as you see, each corner is taking a direct route to its next position.
If we highlight one side, it's maybe a bit more obvious:
and, we can number the corners for even more clarity:
So, if we want to rotate the square without changing its size, we could use keyframe animation and run the corners along a circle:
or, much easier, rotate the mask layer instead of its path:
let radians = 90 / 180.0 * .pi
let anim = CABasicAnimation(keyPath: "transform.rotation.z")
anim.toValue = radians
anim.duration = 1.0
(overlayView.layer.mask as? CAShapeLayer)?.add(anim, forKey: "layerRotate")
No doubt we notice that the mask layer frame does not completely cover the overlayView frame.
So, we can fix that by making the mask layer frame larger.
We could calculate exactly how big it needs to be, but because shape layers are very efficient, we'll just make it a square, 1.5 times bigger than the longer axis:
so when it rotates, it covers the view:
and we finish with this:

Rotate a view from its origin towards another view

I am trying to rotate a view towards another views center point(Remember not around, its towards).
Assume I have 2 views placed like this
Now I want to rotate the topview to point the bottom view like this
so this what I did
Change the top views anchor point to its origin. so that it can rotate and point its edge to the bottom view
Calculated the angle between the first views origin point and the bottom views center
And applied the calculated transform to the top view.
Below the code I am using
let rect = CGRect(x: 70, y: 200, width: 300, height: 100)
let rectView = UIView(frame: rect)
rectView.layer.borderColor = UIColor.green.cgColor;
rectView.layer.borderWidth = 2;
let endView = UIView(frame: CGRect(x: 250, y: 450, width: 70, height: 70))
endView.layer.borderColor = UIColor.green.cgColor;
endView.layer.borderWidth = 2;
let end = endView.center;
self.view.addSubview(endView)
self.view.addSubview(rectView!)
rectView.setAnchorPoint(CGPoint.zero)
let angle = rectView.bounds.origin.angle(to: end);
UIView.animate(withDuration: 3) {
rectView.transform = rectView.transform.rotated(by: angle)
}
I am using this extension from Get angle from 2 positions to calculate the angle between 2 points
extension CGPoint {
func angle(to comparisonPoint: CGPoint) -> CGFloat {
let originX = comparisonPoint.x - self.x
let originY = comparisonPoint.y - self.y
let bearingRadians = atan2f(Float(originY), Float(originX))
var bearingDegrees = CGFloat(bearingRadians).degrees
while bearingDegrees < 0 {
bearingDegrees += 360
}
return bearingDegrees
}
}
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180.0 / M_PI)
}
}
extension UIView {
func setAnchorPoint(_ point: CGPoint) {
var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y);
newPoint = newPoint.applying(transform)
oldPoint = oldPoint.applying(transform)
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.position = position
layer.anchorPoint = point
}
}
But this isn't working as expected, the rotation is way off. Check the below screen capture of the issue
I assume this issue is related to how the angle is calculated, but I could't figure out what?
any help is much appreciated.
Angles need to be in radians
The rotation angle needs to be specified in radians:
Change:
rectView.transform = rectView.transform.rotated(by: angle)
to:
rectView.transform = rectView.transform.rotated(by: angle / 180.0 * .pi)
or change your angle(to:) method to return radians instead of degrees.
Use frame instead of bounds
Also, you need to use the frame of your rectView when computing the angle. The bounds of a view is its internal coordinate space, which means its origin is always (0, 0). You want the frame which is the coordinates of the view in its parent's coordinate system.
Change:
let angle = rectView.bounds.origin.angle(to: end)
to:
let angle = rectView.frame.origin.angle(to: end)
Note: Because your anchorPoint is the corner of rectView, this will point the top edge of rectView to the center of endView. One way to fix that would be to change your anchorPoint to the center of the left edge of rectView and then use that point to compute your angle.

CATransform3DMakeRotation anchorpoint and position in Swift

Hi I am trying to rotate a triangle shaped CAShapeLayer. The northPole CAShapeLayer is defined in a custom UIView class. I have two functions in the class one to setup the layer properties and one to animate the rotation. The rotation animate works fine but after the rotation completes it reverts back to the original position. I want the layer to stay at the angle given (positionTo = 90.0) after the rotation animation.
I am trying to set the position after the transformation to retain the correct position after the animation. I have also tried to get the position from the presentation() method but this has been unhelpful. I have read many articles on frame, bounds, anchorpoints but I still cannot seem to make sense of why this transformation rotation is not working.
private func setupNorthPole(shapeLayer: CAShapeLayer) {
shapeLayer.frame = CGRect(x: self.bounds.width / 2 - triangleWidth / 2, y: self.bounds.height / 2 - triangleHeight, width: triangleWidth, height: triangleHeight)//self.bounds
let polePath = UIBezierPath()
polePath.move(to: CGPoint(x: 0, y: triangleHeight))
polePath.addLine(to: CGPoint(x: triangleWidth / 2, y: 0))
polePath.addLine(to: CGPoint(x: triangleWidth, y: triangleHeight))
shapeLayer.path = polePath.cgPath
shapeLayer.anchorPoint = CGPoint(x: 0.5, y: 1.0)
The animate function:
private func animate() {
let positionTo = CGFloat(DegreesToRadians(value: degrees))
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.fromValue = 0.0
rotate.toValue = positionTo //CGFloat(M_PI * 2.0)
rotate.duration = 0.5
northPole.add(rotate, forKey: nil)
let present = northPole.presentation()!
print("presentation position x " + "\(present.position.x)")
print("presentation position y " + "\(present.position.y)")
CATransaction.begin()
northPole.transform = CATransform3DMakeRotation(positionTo, 0.0, 0.0, 1.0)
northPole.position = CGPoint(x: self.bounds.width / 2, y: self.bounds.height / 2)
CATransaction.commit()
}
To fix the problem, in the custom UIView class I had to override layoutSubviews() with the following:
super.layoutSubviews()
let northPoleTransform: CATransform3D = northPole.transform
northPole.transform = CATransform3DIdentity
setupNorthPole(northPole)
northPole.transform = northPoleTransform

Why the view content is not centered?

This is my result
Although I am explicitly stating that the arc center is the center of the view.
This is my code:
let count = colors.count-1
var index: Int = initialIndex
for oneArray in points {
let startAngleRadiant: CGFloat = degreesToRadians(Double(oneArray[0]))
let endAngleRadiant: CGFloat = degreesToRadians(Double(oneArray[1]))
let radius: CGFloat = self.frame.height/4
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngleRadiant,
endAngle: endAngleRadiant,
clockwise: true)
let color = UIColor(red: colors[count-index][0], green: colors[count-index][1], blue: colors[count-index][2], alpha: 1.0)
path.lineWidth = CGFloat(4)
if ++index >= count {
index = 0
}
color.setStroke()
path.stroke()
}
And the view (that's the circle inside) has the correct constrains for height and width. As you see here
This is the constrain of the view Wainting Circle View
as you see, i am setting the hight and the width relative to the supper view,
thus the center of that view should be correct, as a result, the self.center that i am using should draw at the center
You are defining the center of your arch a self.center which the docs describe as:
The center is specified within the coordinate system of its superview and is measured in points. Setting this property changes the values of the frame properties accordingly.
This is essentially the same as CGPoint(x: frame.midX, y: frame.midY), meaning that if your view isn't flush with the top of the superview superview, center.x is going to be different from the center of the view.
For example, if your view has frame == CGRect(x:50, y:50, width:100, height:100), it will have center == CGPoint(x: 50 + 100/2, y: 50 + 100 /2) == CGPoint(x:100, y:100) which is the bottom right corner of the view.
If you want the circle to be centered in the center of the view, you should pick your center based of the bounds, which is in a view-relative coordinate system:
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngleRadiant,
endAngle: endAngleRadiant,
clockwise: true)

Resources