CGMutablePath.addArc not working in Swift 3? - ios

In Xcode 8 beta 6, some of the functions to add a path changed, including those that add an arc:
func addArc(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool, transform: CGAffineTransform = default)
Beyond a definition of the function, there is no documentation on Apple's site. I've been unable to get an actual arc from this function, and have been relying on a second version that uses tangents. Can anyone provide a working sample? Could it just be bugged?
Here is a function that is broken by the change:
public class func createHorizontalArcPath(_ startPoint:CGPoint, width:CGFloat, arcHeight:CGFloat, closed:Bool = false) -> CGMutablePath
{
// http://www.raywenderlich.com/33193/core-graphics-tutorial-arcs-and-paths
let arcRect = CGRect(x: startPoint.x, y: startPoint.y-arcHeight, width: width, height: arcHeight)
let arcRadius = (arcRect.size.height/2) + (pow(arcRect.size.width, 2) / (8*arcRect.size.height));
let arcCenter = CGPoint(x: arcRect.origin.x + arcRect.size.width/2, y: arcRect.origin.y + arcRadius);
let angle = acos(arcRect.size.width / (2*arcRadius));
let startAngle = CGFloat(M_PI)+angle // (180 degrees + angle)
let endAngle = CGFloat(M_PI*2)-angle // (360 degrees - angle)
let path = CGMutablePath();
path.addArc(center: arcCenter, radius: arcRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
if(closed == true)
{path.addLine(to: startPoint)}
return path;
}

Your Swift code is based on the Objective-C code from http://www.raywenderlich.com/33193/core-graphics-tutorial-arcs-and-paths,
where the arc path is created as
CGPathAddArc(path, NULL, arcCenter.x, arcCenter.y, arcRadius,
startAngle, endAngle, 0);
In particular, 0 is passed as argument to the last parameter bool clockwise. That should be translated to false in Swift, not true:
path.addArc(center: arcCenter, radius: arcRadius,
startAngle: startAngle, endAngle: endAngle, clockwise: false)

Related

doughnut chart with rounded

I want to create doughnut chart with overlapping one side with rounded corner. like
requirement is starting end should be bellow previous line and ending end should above next line. I tired to do it but last line layer show both on top. I tried with bellow code
private func drawCircle(){
var startAngle: CGFloat = .pi / -2
var endAngle: CGFloat = 0.0
drawableItems.forEach { (item) in
print(item.title)
print(startAngle)
let center = calculateCenter()
endAngle = startAngle + calculateAngles(item: item)
if endAngle > 2 * CGFloat.pi {
endAngle -= 2 * .pi
}
// Radius of the view
let radius: CGFloat = calculateRadius()
let arcWidth: CGFloat = self.arcWidth
let circlePath = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
circlePath.lineCapStyle = .round
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.strokeColor = item.color.cgColor
shapeLayer.lineWidth = arcWidth
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = .round
layer.addSublayer(shapeLayer)
startAngle = endAngle
}
}
output gives as bellow. in which last layer with green colour show both end top instead of one bellow previous and other on top of next
please help me to do so.
The issue is that your are drawing a simple arc with a thick line and rounded ends. The two rounded ends of the last arc is always going to cover the previous and first arcs.
The solution is to draw arcs that have a concave end that doesn't cover the convex end of the previous arc.
The following UIBezierPath extension will draw such an arc:
extension UIBezierPath {
convenience init(
chartArcCenter center: CGPoint,
radius: CGFloat,
lineWidth: CGFloat,
startAngle: CGFloat,
endAngle: CGFloat,
clockwise: Bool
) {
self.init()
// Draw inner radius arc
addArc(withCenter: center, radius: radius - lineWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
// Draw convex endcap
var x = cos(endAngle) * radius + center.x
var y = sin(endAngle) * radius + center.y
addArc(withCenter: CGPoint(x: x, y: y), radius: lineWidth / 2, startAngle: endAngle - .pi, endAngle: endAngle, clockwise: !clockwise)
// Draw outer radius arc
addArc(withCenter: center, radius: radius + lineWidth/2, startAngle: endAngle, endAngle: startAngle, clockwise: !clockwise)
// Draw concave startcap
x = cos(startAngle) * radius + center.x
y = sin(startAngle) * radius + center.y
addArc(withCenter: CGPoint(x: x, y: y), radius: lineWidth / 2, startAngle: startAngle, endAngle: startAngle + .pi, clockwise: clockwise)
close()
}
}
Use this in your code instead of the current UIBezierPath call. You can also make changes to the drawing settings of the shape layer.
With all changes in place your method becomes:
private func drawCircle(){
var startAngle: CGFloat = .pi / -2
var endAngle: CGFloat = 0.0
// Radius of the view
let radius: CGFloat = calculateRadius()
let arcWidth: CGFloat = self.arcWidth
let center = calculateCenter()
drawableItems.forEach { (item) in
endAngle = startAngle + calculateAngles(item: item)
let circlePath = UIBezierPath(chartArcCenter: center,
radius: radius/2 - arcWidth/2,
lineWidth: arcWidth,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
let shapeLayer: CAShapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = item.color.cgColor
layer.addSublayer(shapeLayer)
startAngle = endAngle
}
}
Also note that there's no reason for the if endAngle > 2 * CGFloat.pi { block. It's OK if the angle "wraps". And the calculations of radius, width and center only need to be done once so no need for those inside the loop.

Swift: UIBezierPath bezierPathByReversingPath() Issues

I am using bezierPathByReversingPath() trying to make an animating circle. I finally found a method that will work, but I am getting this bar across the circle that is not being "deleted".
The Code:
#IBDesignable class circle: UIView {
override func drawRect(rect: CGRect) {
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let circlePath : UIBezierPath = UIBezierPath(arcCenter: center, radius:
CGFloat(250), startAngle: CGFloat(0), endAngle: CGFloat(360), clockwise: true)
let circlePath2 : UIBezierPath = UIBezierPath(arcCenter: center, radius:
CGFloat(200), startAngle: CGFloat(0), endAngle: CGFloat(360), clockwise: true)
circlePath.appendPath(circlePath2.bezierPathByReversingPath())
circlePath.fill() //appendPath(circlePath.bezierPathByReversingPath())
}
}
The Picture
P.S. If you want an example of what I am doing, it is a win/lose animation in the iOS game "Dulp".
It is because UIBezierPath(arcCenter: center, radius: CGFloat(250), startAngle: CGFloat(0), endAngle: CGFloat(360), clockwise: true) takes radians, not degrees, as you specify here.
Thus, one would need to supply the arguments in radians.
I created a simple function that converts from degrees to radians; the result turned out fine.
Here is the function:
func degreesToRadians(degree : Int) -> CGFloat
{
return CGFloat(degree) * CGFloat(M_PI) / 180.0
}
Usage:
let circlePath : UIBezierPath = UIBezierPath(arcCenter: center, radius: 250.0, startAngle: self.degreesToRadians(0), endAngle:
self.degreesToRadians(360), clockwise: true)
let circlePath2 : UIBezierPath = UIBezierPath(arcCenter: center, radius: 200.0, startAngle: self.degreesToRadians(0), endAngle:
self.degreesToRadians(360), clockwise: true)
Or one could create a protocol extension:
extension Double
{
var degreesToRad : CGFloat
{
return CGFloat(self) * CGFloat(M_PI) / 180.0
}
}
Usage:
let circlePath : UIBezierPath = UIBezierPath(arcCenter: center, radius: 250.0, startAngle: 0.0.degreesToRad , endAngle: 360.0.degreesToRad, clockwise: true)
let circlePath2 : UIBezierPath = UIBezierPath(arcCenter: center, radius: 200.0, startAngle: 0.0.degreesToRad, endAngle: 360.0.degreesToRad, clockwise: true)
First of all, the angles are in radians - not degrees. So your circles are going around 30 or so times and not ending up where they started. That's what gives you the bar.
Secondly, you can just stroke one circle instead of creating two and filling them:
#IBDesignable class Circle: UIView {
override func drawRect(rect: CGRect) {
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
let circlePath : UIBezierPath = UIBezierPath(arcCenter: center, radius:
CGFloat(225), startAngle: CGFloat(0), endAngle: CGFloat(2*M_PI), clockwise: true)
circlePath.lineWidth = 50
circlePath.stroke()
}
}

Draw a semi-circle button iOS

I am trying to draw a semi-circle button. I'm having trouble both drawing the semi-circle and attaching it to the button I made in xcode. The button in xcode has constraints pinning it to the bottom of the screen and centering it. I reference the button in my view controller and then try to override it to a semi-circle as follows. I am getting a blank button. I also hard coded in the coordinates from the storyboard of where the button is. Is there a better way to do that?
let circlePath = UIBezierPath.init(arcCenter: CGPoint(x: 113.0, y: 434.0),
radius: 10.0, startAngle: 0.0, endAngle: CGFloat(M_PI), clockwise: true)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.CGPath
sunButton.layer.mask = circleShape
The problem is that your shape layer is positioned way outside the buttons bounds. If you just add the shape layer as a subview of your button's layer you'll see the problem:
button.layer.addSublayer(circleShape)
You have to adjust the arc center point, so that the arc lies within the button's bounds:
let circlePath = UIBezierPath.init(arcCenter: CGPointMake(button.bounds.size.width / 2, 0), radius: button.bounds.size.height, startAngle: 0.0, endAngle: CGFloat(M_PI), clockwise: true)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.CGPath
button.layer.mask = circleShape
And you'll get this:
Swift 5 update:
let circlePath = UIBezierPath.init(arcCenter: CGPoint(x: button.bounds.size.width / 2, y: 0), radius: button.bounds.size.height, startAngle: 0.0, endAngle: .pi, clockwise: true)
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
button.layer.mask = circleShape
Swift 5:
// semiCircleDown
let circlePath = UIBezierPath(arcCenter: CGPoint(x: shapeView.bounds.size.width/2, y: 0), radius: shapeView.bounds.size.height, startAngle: 0, endAngle: .pi, clockwise: true).cgPath
//semiCircleUp
let circlePath = UIBezierPath(arcCenter: CGPoint(x: shapeView.bounds.size.width/2, y: 0), radius: shapeView.bounds.size.height, startAngle: 2 * .pi, endAngle: .pi, clockwise: true).cgPath
//quarterCircleRightDown
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: shapeView.bounds.size.width, startAngle: 2 * .pi, endAngle: .pi * 3/2, clockwise: true).cgPath
//quarterCircleLeftDown
let circlePath = UIBezierPath(arcCenter: CGPoint(x: shapeView.bounds.size.width, y: 0), radius: shapeView.bounds.size.width, startAngle: .pi * 3/2, endAngle: .pi, clockwise: true).cgPath
quarterCircleRightUp
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: shapeView.bounds.size.height), radius: shapeView.bounds.size.width, startAngle: 0, endAngle: .pi/2, clockwise: false).cgPath
//quarterCircleLeftUp
let circlePath = UIBezierPath(arcCenter: CGPoint(x: shapeView.bounds.size.width, y: shapeView.bounds.size.height), radius: shapeView.bounds.size.width, startAngle: .pi/2, endAngle: .pi, clockwise: false).cgPath
let shape = CAShapeLayer()
shape.path = circlePath.cgPath
shapeView.layer.mask = shape

Animate CALayer shadow from square to circle (or rounded corners)

I have a UIImageView, which animates from square to circle with this code:
CABasicAnimation *circleShapeAnimation = [CABasicAnimation animationWithKeyPath:#"cornerRadius"];
circleShapeAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
circleShapeAnimation.toValue = [NSNumber numberWithFloat:cornerRadius];
I also have a separate UIView (its backgroundColor is clearColor), which I use specifically to achieve an animated shadow. Everything animates fine, except the shape animation:
CABasicAnimation* shadowShapeAnimation = [CABasicAnimation animationWithKeyPath: #"shadowPath"];
shadowShapeAnimation.fromValue = (id)[UIBezierPath bezierPathWithRoundedRect: cell.placeImage.bounds cornerRadius:0.0f].CGPath;
shadowShapeAnimation.toValue = (id)[UIBezierPath bezierPathWithRoundedRect: cell.placeImage.bounds cornerRadius:cornerRadius].CGPath;
which, simply put, looks uglier than me with a hangover in the morning.
I would like to achieve a nice, smooth animation of rounded corners. Any idea how to?
The key was to use those methods:
func circlePathWithCenter(center: CGPoint, radius: CGFloat) -> UIBezierPath {
let circlePath = UIBezierPath()
circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI), endAngle: -CGFloat(M_PI/2), clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: -CGFloat(M_PI/2), endAngle: 0, clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: 0, endAngle: CGFloat(M_PI/2), clockwise: true)
circlePath.addArcWithCenter(center, radius: radius, startAngle: CGFloat(M_PI/2), endAngle: CGFloat(M_PI), clockwise: true)
circlePath.closePath()
return circlePath
}
func squarePathWithCenter(center: CGPoint, side: CGFloat) -> UIBezierPath {
let squarePath = UIBezierPath()
let startX = center.x - side / 2
let startY = center.y - side / 2
squarePath.moveToPoint(CGPoint(x: startX, y: startY))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX + side, y: startY + side))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.addLineToPoint(CGPoint(x: startX, y: startY + side))
squarePath.addLineToPoint(squarePath.currentPoint)
squarePath.closePath()
return squarePath
}
Found here: iOS animate/morph shape from circle to square

Circular Progress Bar help, Swift Programming Language

I'm trying to make a small circular progress bar in swift,
This is what I have so far
private var Timer:CGFloat = 100;
override func update(currentTime: CFTimeInterval)
{
/* Called before each frame is rendered */
var StartAngle:CGFloat!
var EndAngle:CGFloat!
var circleTimer:UIBezierPath!
StartAngle = CGFloat(M_PI * 1.5)
EndAngle = StartAngle + CGFloat((M_PI * 2))
var Progress:CGFloat = (EndAngle - StartAngle) * (Timer / 100) + StartAngle
circleTimer.addArcWithCenter(CGPointMake(200, 200), radius: CGFloat(130), startAngle: StartAngle, endAngle: Progress, clockwise: true)
Timer--;
}
I get this error when I run the App
"BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)"
on this line
"circleTimer.addArcWithCenter(CGPointMake(200, 200), radius: CGFloat(130), startAngle: StartAngle, endAngle: Progress, clockwise: true)"
I'm fairly new to Swift (Programmed in other languages) and I can't seem to work it out.
Thanks
Luke
You have not initialized circleTimer so circleTimer is nil.You should initialize it first than call
circleTimer.addArcWithCenter(CGPointMake(200, 200), radius: CGFloat(130), startAngle: StartAngle, endAngle: Progress, clockwise: true)
Intialize it with path
var circleTimer:UIBezierPath! = UIBezierPath()
Also write this in draw rect
override func drawRect(rect: CGRect) {
/* Called before each frame is rendered */
var StartAngle:CGFloat!
var EndAngle:CGFloat!
var circleTimer:UIBezierPath! = UIBezierPath()
StartAngle = CGFloat(M_PI * 1.5)
EndAngle = StartAngle + CGFloat((M_PI * 2))
var Progress:CGFloat = (EndAngle - StartAngle) * (Timer / 100) + StartAngle
//Test this working fine
//circleTimer.addArcWithCenter(CGPointMake(50, 50), radius: CGFloat(130), startAngle: 0.0, endAngle: Progress, clockwise: true)
circleTimer.addArcWithCenter(CGPointMake(200, 200), radius: CGFloat(130), startAngle: StartAngle, endAngle: Progress, clockwise: true)
Timer--;
circleTimer.lineWidth = 20;
UIColor.redColor().setStroke()
circleTimer.stroke()
}

Resources