Circular Progress Bar help, Swift Programming Language - ios

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()
}

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.

UIBezierPath animation from circle to rounded square

I'm trying to animate from a circle to a square with rounded corners. As far as I understand, number of points from both paths should be equal for smooth animation. So I wrote a function that builds a circle bases on a number of arcs:
private func circleShape(segmentNumber: Int = 8, radius: CGFloat = 32) -> CGPath {
let center = self.bounds.center
let circlePath = UIBezierPath()
let segmentAngle = CGFloat.pi * CGFloat(2) / CGFloat(segmentNumber)
var currentAngle: CGFloat = 0
for _ in 0 ..< segmentNumber {
let nextAngle = currentAngle + segmentAngle
circlePath.addArc(withCenter: center, radius: radius,
startAngle: currentAngle, endAngle: nextAngle, clockwise: true)
currentAngle = nextAngle
}
circlePath.close()
return circlePath.cgPath
}
I'm checking number of points with getPathElementsPointsAndTypes() and it increases unproportionally to the segmentNumber (segmentNumber: 1, points: 13; sN: 2, p: 14; sN: 3, p: 21; sN: 4, p: 16). So I can't find a segmentNumber that would give the same number of points (19) as the function that draws the square:
private func squareShape(size: CGFloat = 44, radius: CGFloat = 8) -> CGPath {
let rect = CGRect(center: self.bounds.center, size: CGSize(width: size, height: size))
let left = CGFloat.pi
let up = CGFloat(1.5) * CGFloat.pi
let down = CGFloat.pi / CGFloat(2)
let right = CGFloat(0.0)
let squarePath = UIBezierPath()
squarePath.addArc(withCenter: CGPoint(x: rect.minX + radius, y: rect.minY + radius), radius: radius, startAngle: left, endAngle: up, clockwise: true)
squarePath.addArc(withCenter: CGPoint(x: rect.maxX - radius, y: rect.minY + radius), radius: radius, startAngle: up, endAngle: right, clockwise: true)
squarePath.addArc(withCenter: CGPoint(x: rect.maxX - radius, y: rect.maxY - radius), radius: radius, startAngle: right, endAngle: down, clockwise: true)
squarePath.addArc(withCenter: CGPoint(x: rect.minX + radius, y: rect.maxY - radius), radius: radius, startAngle: down, endAngle: left, clockwise: true)
squarePath.close()
return squarePath.cgPath
}
Animation code:
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 2
animation.toValue = self.circleShape()
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
self.testLayer.add(animation, forKey: "pathAnimation")
Result:
What could I change to make the animation same smooth as in example without rounded rects?
You can simply have two paths
func circlePath () -> UIBezierPath {
return UIBezierPath(roundedRect: CGRect(x:0, y:0, width:50, height:50), cornerRadius: 25)
}
func squarePath () -> UIBezierPath {
return UIBezierPath(roundedRect: CGRect(x:0, y:0, width:30, height:30), cornerRadius: 4)
}

CGMutablePath.addArc not working in Swift 3?

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)

iOS - UIBezierPath making start angle always vertical

Hi I'm currently trying to make random angles of a circle but id like the startangle to always be vertical. In the image above its shows 180 degrees of a circle but the start angle is horizontal and it changes depending on the angle i specify. So basically i want the start of the circle to always start at 0 degrees or vertical. Does anyone know the solution for this?
This is the code I'm using.
let center = CGPointMake(size.width/2, size.height/2)
let path = UIBezierPath()
path.addArcWithCenter(center, radius: 200, startAngle: 0, endAngle: 180.degreesToRadians, clockwise: true)
path.addLineToPoint(center)
path.closePath()
let angleShape = SKShapeNode(path: path.CGPath)
angleShape.strokeColor = UIColor(red: 0.203, green: 0.596, blue: 0.858, alpha: 1.0)
angleShape.fillColor = UIColor(red: 0.203, green: 0.596, blue: 0.858, alpha: 1.0)
addChild(angleShape)
change this:
path.addArcWithCenter(center, radius: 200, startAngle: 0, endAngle: 180.degreesToRadians, clockwise: true)
to this:
path.addArcWithCenter(center, radius: 200, startAngle: CGFloat(M_PI_2), endAngle: CGFloat(M_PI + M_PI_2), clockwise: true)
if you want a random startAngle and endAngle try this
func randomRange(min min: CGFloat, max: CGFloat) -> CGFloat {
assert(min < max)
return CGFloat(Float(arc4random()) / 0xFFFFFFFF) * (max - min) + min
}
let startAngle = randomRange(min: CGFloat(0), max: CGFloat(M_PI*2))
let endAngle = randomRange(min: CGFloat(0), max: CGFloat(M_PI*2))
path.addArcWithCenter(center, radius: 200, startAngle: startAngle, endAngle: endAngle, clockwise: true)

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()
}
}

Resources