How to rotate UIVIew in its top left coordinates? - ios

Existing CGAffineTransform performs this job well, But it only rotates in its center points. I want to rotate the UIView in its Top-Left coordinates,
Rotation Code:
// here rectangle is UIView
rectangle.transform = CGAffineTransform(rotationAngle: 90.degreesToRadians)
Extension:
// use to convert degree to radian
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
I cant find my answer in suggested question.

Just change anchorPoint of layer before transformation. For example:
rectangle.layer.anchorPoint = CGPoint.zero
rectangle.transform = CGAffineTransform(rotationAngle: 90.degreesToRadians)

Thanks Here Trix and the4kman for the idea. I solved my problem. Here Trix answer not fully helped me. But He shows some hints. Using that hints I solved my answer.
Step 1
Create this extension for UIView to set anchorPoint
extension UIView{
func setAnchorPoint(anchorPoint: CGPoint) {
var newPoint = CGPoint(x: self.bounds.size.width * anchorPoint.x, y: self.bounds.size.height * anchorPoint.y)
var oldPoint = CGPoint(x: self.bounds.size.width * self.layer.anchorPoint.x, y: self.bounds.size.height * self.layer.anchorPoint.y)
newPoint = newPoint.applying(self.transform)
oldPoint = oldPoint.applying(self.transform)
var position : CGPoint = self.layer.position
position.x -= oldPoint.x
position.x += newPoint.x;
position.y -= oldPoint.y;
position.y += newPoint.y;
self.layer.position = position;
self.layer.anchorPoint = anchorPoint;
}
}
Usage
rectangle.setAnchorPoint(anchorPoint: CGPoint.zero)
rectangle.transform = CGAffineTransform(rotationAngle: 90.degreesToRadians)
This working perfectly.

Related

Facing Issue with CGAffineTransform in NIB

I have 2 Views (viewLeft, viewRight). I am rotating these views with angel 0 (initial setup) by calculating the same anchor point so that it looks like views are rotating across hinge.
I have done below code, which is working fine if I took the views in storyboard (viewcontroller) and it does not work if I copy and paste same views and code in nib. In Nib, at 0 angel views leaves some space between them. It is weird behavior, I checked all code outlets and constraints, everything is fine but unable to figure out why it is not working.
I want it to be working in NIB.
Any help will be appreciable.
Github dummy project reference here
Outputs :
From Storyboard
From XIB
Code work
Set Anchor point
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
}
}
Rotation function
func rotateView(_ addAngel : CGFloat) {
let rotateToAngel = angel + addAngel
guard rotateToAngel <= 0.6 && rotateToAngel >= -0.6 else {
return
}
angel = rotateToAngel
let anchorPointLeft = CGPoint(x: (viewLeft.frame.width - viewLeft.frame.height/2)/viewLeft.frame.width,
y: ( viewLeft.frame.height/2)/viewLeft.frame.height)
viewLeft.setAnchorPoint(anchorPointLeft)
let anchorPointRight = CGPoint(x: (viewRight.frame.height/2/viewRight.frame.width),
y: ( viewRight.frame.height/2)/viewRight.frame.height)
viewRight.setAnchorPoint(anchorPointRight)
self.viewLeft.transform = CGAffineTransform.init(rotationAngle: rotateToAngel)
self.viewRight.transform = CGAffineTransform.init(rotationAngle: -rotateToAngel)
}

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.

How to make stretching a circle effect on ios

I have an UIView with bounding to a circle, something like this
I want to make an effect like stretching it, see the following image
It likes a pullToRefresh effect, there are many libraries or opensources to make pullToRefresh, but they're always belongs to tableview. I just want to make the stretching effect independently. I just have a circled UIView and it's stretched when I pull it
How can I make it
The basic idea is to use bezier paths to outline the precise shape you're looking for. You can then use gesture to change that shape and use display link to animate the returning of the stretched circle back to its circular form:
#IBDesignable
class RefreshView: UIView {
private let shapeLayer = CAShapeLayer()
#IBInspectable
var fillColor: UIColor = UIColor.lightGrayColor() {
didSet {
shapeLayer.fillColor = fillColor.CGColor
}
}
#IBInspectable
var strokeColor: UIColor = UIColor.clearColor() {
didSet {
shapeLayer.strokeColor = strokeColor.CGColor
}
}
#IBInspectable
var lineWidth: CGFloat = 0.0 {
didSet {
shapeLayer.strokeColor = strokeColor.CGColor
}
}
/// Center of main circle is in center top
private var pullDownCenter: CGPoint {
return CGPoint(x: bounds.size.width / 2.0, y: bounds.size.width / 2.0)
}
/// Radius of view spans width of view
private var radius: CGFloat {
return bounds.size.width / 2.0
}
override var frame: CGRect {
get {
return super.frame
}
set {
super.frame = newValue
updatePath()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
configureView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configureView()
}
/// Update the path, add shape layer, and add gesture recognizer
private func configureView() {
shapeLayer.fillColor = fillColor.CGColor
shapeLayer.strokeColor = strokeColor.CGColor
shapeLayer.lineWidth = lineWidth
updatePath()
layer.addSublayer(shapeLayer)
let pan = UIPanGestureRecognizer(target: self, action: #selector(RefreshView.handlePan(_:)))
addGestureRecognizer(pan)
}
/// Update path
private func updatePath() {
shapeLayer.path = stretchyCirclePathWithCenter(pullDownCenter, radius: radius, yOffset: yOffset).CGPath
}
override func prepareForInterfaceBuilder() {
super.prepareForInterfaceBuilder()
yOffset = yOffsetMax
}
// MARK: Gesture Recognizer
private var yOffset: CGFloat = 0.0 { didSet { updatePath() } }
private var yOffsetMax: CGFloat { return bounds.size.width * 1.5 }
private var yOldOffset: CGFloat = 0.0
func handlePan(gesture: UIPanGestureRecognizer) {
if gesture.state == .Began {
yOldOffset = yOffset
} else if gesture.state == .Changed {
yOffset = yOldOffset + max(0, min(gesture.translationInView(gesture.view).y, yOffsetMax))
} else if gesture.state == .Ended || gesture.state == .Cancelled {
animateBackToCircle()
}
}
// MARK: Animation
private var displayLink: CADisplayLink?
private var duration: CGFloat?
private var startTime: CFAbsoluteTime?
private var originalOffset: CGFloat?
private func animateBackToCircle() {
displayLink = CADisplayLink(target: self, selector: #selector(RefreshView.handleDisplayLink(_:)))
duration = 0.5
originalOffset = yOffset
startTime = CFAbsoluteTimeGetCurrent()
displayLink?.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
func handleDisplayLink(displayLink: CADisplayLink) {
let percent = CGFloat(CFAbsoluteTimeGetCurrent() - startTime!) / duration!
if percent < 1.0 {
yOffset = originalOffset! * (1.0 - sin(percent * CGFloat(M_PI_2)))
} else {
self.displayLink?.invalidate()
self.displayLink = nil
updatePath()
}
}
// MARK: Stretch circle path
private func stretchyCirclePathWithCenter(center: CGPoint, radius: CGFloat, yOffset: CGFloat = 0.0) -> UIBezierPath {
func pointWithCenter(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
return CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
}
if yOffset == 0 {
return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2.0 * CGFloat(M_PI), clockwise: true)
}
let lowerRadius = radius * (1 - yOffset / yOffsetMax * 0.5)
let yOffsetTop = yOffset / 4
let yOffsetBottom = yOffset / 1.5
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(M_PI), endAngle: 0, clockwise: true)
path.addCurveToPoint(CGPoint(x: center.x + lowerRadius, y:center.y + yOffset), controlPoint1: CGPoint(x: center.x + radius, y:center.y + yOffsetTop), controlPoint2: CGPoint(x: center.x + lowerRadius, y:center.y + yOffset - yOffsetBottom))
path.addArcWithCenter(CGPoint(x: center.x, y:center.y + yOffset), radius: lowerRadius, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
path.addCurveToPoint(CGPoint(x: center.x - radius, y:center.y), controlPoint1: CGPoint(x: center.x - lowerRadius, y:center.y + yOffset - yOffsetBottom), controlPoint2: CGPoint(x: center.x - radius, y:center.y + yOffsetTop))
return path
}
}
This renders something like:
Clearly, feel free to play around with the path as you see fit, add additional flourishes like a spinning arrow or whatever, etc. But this illustrates the basics of constructing bezier path, stretching it with gesture, and animating it back to its circular shape.
There is already a control available for you:
circle with strech
Apart from above i will give a try to this approach also:
I think you can have full streched image, hidden by white view above it. As user swips down or up you can show/hide part of streched image.
if user have not swiped then show unstreched circle image, and if user have swipe a thresold distance show fully streched image.
You will likely need to do custom drawing. Just stretching a view will cause there to be aliasing and the view to be misshaped.
To do this you should subclass UIView and override drawRect to do some kind of drawing that can respond to user interaction.
I've created a sample class:
class CircleRefresh:UIView{
var taperFactor:CGFloat = 0.00{
didSet{
self.setNeedsDisplay()
}
}
var heightFactor: CGFloat = 0.40
{
didSet{
self.setNeedsDisplay()
}
}
var radius:CGFloat
{
return self.frame.size.height * 0.20
}
//Set this to add an arrow or another image to the top circle
var refreshImage:UIImage?{
didSet{
//
self.subviews.forEach{$0.removeFromSuperview()}
if let image = self.refreshImage{
let imageView = UIImageView(frame: CGRect(x:self.frame.size.width * 0.5 - self.radius,y:0.0,width:self.radius * 2.0,height:self.radius * 2.0))
imageView.image = image
imageView.contentMode = .ScaleAspectFit
imageView.layer.cornerRadius = imageView.frame.size.height * 0.5
imageView.layer.masksToBounds = true
self.addSubview(imageView)
}
}
}
override func drawRect(rect:CGRect)
{
UIColor.lightGrayColor().setFill()
let endCenter = CGPoint(x:self.frame.size.width * 0.5,y:self.frame.size.height * heightFactor - radius * taperFactor)
//top arc
let path = UIBezierPath(arcCenter: CGPoint(x:self.frame.size.width * 0.5, y:radius), radius: self.frame.size.height * 0.20, startAngle: 0.0, endAngle: CGFloat(M_PI), clockwise: false)
//left curve
path.addCurveToPoint(CGPoint(x:endCenter.x - radius * taperFactor,y:endCenter.y), controlPoint1: CGPoint(x:endCenter.x - radius, y:radius * 2.0), controlPoint2: CGPoint(x: endCenter.x - radius * taperFactor, y: radius * 2.0))
//bottom arc
path.addArcWithCenter(endCenter, radius: radius * taperFactor, startAngle: CGFloat(M_PI), endAngle: 0.0, clockwise: false)
//right curve
path.addCurveToPoint(CGPoint(x:endCenter.x + radius,y:radius), controlPoint1: CGPoint(x: endCenter.x + radius * taperFactor, y: radius * 2.0),controlPoint2: CGPoint(x:endCenter.x + radius, y:radius * 2.0))
path.fill()
}
}
This will draw a circle but changing taperFactor and heightFactor will give a stretching effect. Setting heightFactor to 1.0 will mean the drawing will take up the entire height of the view and setting taperFactor to 1.0 will cause the tail of the drawing to be as wide as the top circle.
To get a sense for how changing heightFactor and taperFactor changes the drawing, you can then add a pan gesture recognizer to this view and in it's action say something like:
#IBAction func pan(sender: UIPanGestureRecognizer) {
let location = sender.locationInView(self.view)
circle.heightFactor = location.y / self.view.frame.size.height
circle.taperFactor = circle.heightFactor * 0.5 - 0.20
}
You can change heightFactor and taperFactor to achieve different effects. This is only part of how to make a refresh controller but seems to be the thrust of your question.

UIView layer moves when rotating about non-centre anchor point

I have a UIView which covers another UIView. When the covering view is tapped, I want it to “fall”, this is where we animate it around its x axis about the bottom centre point.
I’ve implemented this like so:
private func showCategories(animated: Bool)
{
UIView.animateWithDuration(2,
delay: 0,
options: [.CurveEaseIn],
animations: {
self.setAnchorPoint(CGPoint(x: 0.5, y: 1), forView: self.titleView)
var rotationAndPerspectiveTransform = CATransform3DIdentity
rotationAndPerspectiveTransform.m34 = 1 / -500
rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, CGFloat(-179 * M_PI / 180), 1, 0, 0);
self.titleView.layer.transform = rotationAndPerspectiveTransform;
}) { (success) -> Void in
//
}
}
I found a solution which said that changing the anchor point moves the view, so to fix it you need to move it taking in to consider the anchor point and transform. It provided this code:
private func setAnchorPoint(anchorPoint: CGPoint, forView view: UIView)
{
var newPoint = CGPoint(x: view.bounds.width * anchorPoint.x, y: view.bounds.height * anchorPoint.y)
var oldPoint = CGPoint(x: view.bounds.width * view.layer.anchorPoint.x, y: view.bounds.height * view.layer.anchorPoint.y)
newPoint = CGPointApplyAffineTransform(newPoint, view.transform);
oldPoint = CGPointApplyAffineTransform(oldPoint, view.transform);
var position = view.layer.position;
position.x -= oldPoint.x;
position.x += newPoint.x;
position.y -= oldPoint.y;
position.y += newPoint.y;
view.layer.position = position;
view.layer.anchorPoint = anchorPoint;
}
However, this still doesn’t work for me. Am I using it wrong?
P.s, I realise the animated parameter isn’t being taken in to account yet… It’s for debugging purposes.
Okay, so moving the setAnchor:forView: method to before the animation block works.
My bad.

iOS Swift Xcode 6: CGAffineTransformRotate with auto-layout anchorpoint

I'm making an app with a rotatable pie chart. However, my pie chart rotates around (0,0) and I can't find a solution to make it rotate around its center.
Some code:
// DRAWING CODE
// Set constants for piePieces
let radius: CGFloat = CGFloat(screen.width * 0.43)
let pi: CGFloat = 3.1415926535
let sliceRad: CGFloat = 2.0 * pi / CGFloat(categoryArray.count)
var currentAngle: CGFloat = -0.5 * sliceRad - 0.5 * pi
let center: CGPoint = CGPoint(x: screen.width / 2.0, y: radius)
println("Center point: \(center)")
//container!.layer.anchorPoint = CGPoint(x: screen.width, y: radius)
// Draw all pie charts, add them to container (chartView)
for category in categoryArray {
let slice = UIView()
slice.frame = self.frame
let shapeLayer = CAShapeLayer()
let scoreIndex = CGFloat(category.calculateScoreIndex())
let sliceRadius: CGFloat = scoreIndex * radius
// Draw the path
var path:UIBezierPath = UIBezierPath()
path.moveToPoint(center)
path.addLineToPoint(CGPoint(x: center.x + sliceRadius * cos(currentAngle), y: center.y + sliceRadius * sin(currentAngle)))
path.addArcWithCenter(center, radius: sliceRadius, startAngle: currentAngle, endAngle: currentAngle + sliceRad, clockwise: true)
path.addLineToPoint(center)
path.closePath()
// For next slice, add 2*pi Rad / n categories
currentAngle += sliceRad
// Add path to shapeLayer
shapeLayer.path = path.CGPath
//shapeLayer.frame = self.frame
shapeLayer.fillColor = SurveyColors().getColor(category.categoryIndex).CGColor
shapeLayer.anchorPoint = center
// Add shapeLayer to sliceView
slice.layer.addSublayer(shapeLayer)
// Add slice to chartView
container!.addSubview(slice)
}
self.addSubview(container!)
//container!.center = center
container!.layer.anchorPoint = center
}
I sheduled a NSTimer to perform a rotation every 2 seconds (for testing purposes):
func rotate() {
let t: CGAffineTransform = CGAffineTransformRotate(container!.transform, -0.78)
container!.transform = t;
}
The pie chart rotates around (0,0). What is going wrong?
I believe a good solution to your problem would be to set a container view:
container = UIView()
container!.frame = CGRect(x: 0.0, y: 0.0, width: screen.width, height: screen.width)
container!.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
container!.backgroundColor = UIColor.clearColor()
And then add all slices to this container view:
let slice = UIView()
slice.frame = CGRect(x: 0.0 , y: 0.0 , width: container!.bounds.width, height: container!.bounds.height)
let shapeLayer = CAShapeLayer()
let scoreIndex = CGFloat(category.calculateScoreIndex())
let sliceRadius: CGFloat = scoreIndex * radius
/*
*/
container!.addSubview(slice)
Also, make sure not to add your chart as a subview to a view with auto-layout constraints (which might interfere with you CGAffineTransformRotate).

Resources