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.
Related
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)
}
One of the app I'm using has bar graph in circular shape. I've tried to create this by using UIBezierPath with arcCenter but I felt like this is completely wrong approach.
The code I'm currently using:
func drawGraph(){
var startDegrees = 120
for _ in 0...10{
let startAngle: CGFloat = radians(of: startDegrees)
let endAngle: CGFloat = radians(of: startDegrees + 5)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(center.x, center.y) - trackBorderWidth / 2 - CGFloat(Int.random(in: 10...50))
let trackPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackPath.lineWidth = trackBorderWidth
trackPath.lineCapStyle = .square
trackBackgroundColor.set()
trackPath.stroke()
startDegrees += 7
}
}
Do you have better solutions or do you know any similar graph libraries like this?
EDIT:
So I've been trying to change my code little bit, I'm able to create something similar with this code:
func drawGraph(){
var temp:CGFloat = 120.0
for i in 0...64{
let startAngle: CGFloat = radians(of: temp)
let endAngle: CGFloat = radians(of: temp + 5)
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(center.x, center.y) - trackBorderWidth / 2 - 10
let randomNumber = CGFloat(Int.random(in: 10...20))
let trackPath = UIBezierPath(arcCenter: center, radius: radius + randomNumber / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
trackPath.lineWidth = randomNumber
trackPath.lineCapStyle = .butt
UIColor.randomColor().set()
trackPath.stroke()
temp += 5.5
}
let circlePath = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: 101, startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//change the fill color
shapeLayer.fillColor = UIColor.clear.cgColor
//you can change the stroke color
shapeLayer.strokeColor = UIColor.red.cgColor
//you can change the line width
shapeLayer.lineWidth = 3.0
self.layer.addSublayer(shapeLayer)
}
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()
}
}
I'm new to swift and to development at all.
I'm trying to create a wheel fortune for my app but I'm having some difficulties. I need to be able to set the number of sections of the wheel each time differently. Any help how to do it?
I tried to create ellipse like that:
import UIKit
class addShape: UIView {
let context = UIGraphicsGetCurrentContext();
override func drawRect(rect: CGRect) {
CGContextSetLineWidth(context, 3.0)
//CGContextSetStrokeColorWithColor(context, UIColor.purpleColor().CGColor)
CGContextStrokeEllipseInRect(context, rect)
UIColor.redColor().set()
//Actually draw the path
CGContextStrokePath(context)
}
}
but I could not create the ellipse at all :(
I tried this way as well:
override func drawRect(rect: CGRect) {
////UTUBE
UIGraphicsBeginImageContextWithOptions(CGSize(width: 512, height: 512), false, 0)
// Define the center point of the view where you’ll rotate the arc around
let center = CGPoint(x:bounds.width/2, y: bounds.height/2)
// Calculate the radius based on the max dimension of the view.
let radius: CGFloat = max(bounds.width, bounds.height)
// Define the thickness of the arc.
let arcWidth: CGFloat = 115
/////FIRST Shape
// Define the start and end angles for the arc.
let startAngle: CGFloat = 2 * π
let endAngle: CGFloat = π / 4
// Create a path based on the center point, radius, and angles you just defined.
var path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// Set the line width and color before finally stroking the path.
path.lineWidth = arcWidth
counterColor.setStroke()
path.stroke()
// Second Shape
let startAngle2: CGFloat = π / 4
let endAngle2: CGFloat = π / 2
// Create a path based on the center point, radius, and angles you just defined.
path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle2,
endAngle: endAngle2,
clockwise: true)
path.lineWidth = arcWidth
UIColor.greenColor().setStroke()
path.stroke()
// 3rd Shape
let startAngle3: CGFloat = π / 2
let endAngle3: CGFloat = 3 * π / 4
// Create a path based on the center point, radius, and angles you just defined.
path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle3,
endAngle: endAngle3,
clockwise: true)
path.lineWidth = arcWidth
UIColor.blueColor().setStroke()
path.stroke()
// 4th Shape
let startAngle4: CGFloat = 3 * π / 4
let endAngle4: CGFloat = π
// Create a path based on the center point, radius, and angles you just defined.
path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle4,
endAngle: endAngle4,
clockwise: true)
path.lineWidth = arcWidth
UIColor.redColor().setStroke()
path.stroke()
// 5th Shape
let startAngle5: CGFloat = π
let endAngle5: CGFloat = 5 * π / 4
// Create a path based on the center point, radius, and angles you just defined.
path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle5,
endAngle: endAngle5,
clockwise: true)
path.lineWidth = arcWidth
UIColor.yellowColor().setStroke()
path.stroke()
// 6th Shape
let startAngle6: CGFloat = 5 * π / 4
let endAngle6: CGFloat = 3 * π / 2
// Create a path based on the center point, radius, and angles you just defined.
path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle6,
endAngle: endAngle6,
clockwise: true)
path.lineWidth = arcWidth
UIColor.grayColor().setStroke()
path.stroke()
// 7th Shape
let startAngle7: CGFloat = 3 * π / 2
let endAngle7: CGFloat = 7 * π / 4
// Create a path based on the center point, radius, and angles you just defined.
path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle7,
endAngle: endAngle7,
clockwise: true)
path.lineWidth = arcWidth
UIColor.purpleColor().setStroke()
path.stroke()
// 8th Shape
let startAngle8: CGFloat = 7 * π / 4
let endAngle8: CGFloat = 2 * π
// Create a path based on the center point, radius, and angles you just defined.
path = UIBezierPath(arcCenter: center,
radius: radius/2 - arcWidth/2,
startAngle: startAngle8,
endAngle: endAngle8,
clockwise: true)
path.lineWidth = arcWidth
UIColor.lightGrayColor().setStroke()
path.stroke()
}
I don't think that's the way to do it, but I did not find any other way to do this.
The main problem with your first example is that your context needs to be retrieved as part of drawRect. Once you have a circle, creating n segments is just a question of drawing n-1 lines.
(Insetting for line width increases complication but looks nicer.)
class WheelView: UIView {
var pieSections: Int = 1
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext();
let lineWidth: CGFloat = 3.0
// Draw outline
CGContextSetLineWidth(context, lineWidth)
UIColor.redColor().set()
let smaller = rect.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0)
CGContextStrokeEllipseInRect(context, smaller)
let radius = smaller.width / 2.0
let segmentAngle = 2.0 / CGFloat(pieSections) * CGFloat(M_PI)
// Draw segment dividers
for index in 1...pieSections {
let drawAngle = segmentAngle * CGFloat(index)
let x = radius * cos(drawAngle) + rect.width / 2.0
let y = radius * sin(drawAngle) + rect.height / 2.0
CGContextBeginPath(context)
CGContextMoveToPoint(context, rect.width / 2, rect.height / 2)
CGContextAddLineToPoint(context, x, y)
CGContextStrokePath(context)
}
}
}
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