Circular Bar Graph - ios

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

Related

Add Shadow to CAShapeLayer with UIBezierPath

need some help adding a drop shadow to a line created with a CAShapeLayer / UIBezierPath
This code snippet produces the following line shape as in the screenshot below
let bounds = UIScreen.main.bounds
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 10.0
shapeLayer.frame = CGRect(x: 0, y: 0 , width: bounds.width, height: bounds.width)
shapeLayer.lineWidth = 5.0
shapeLayer.strokeColor = UIColor.red.cgColor
let offset : CGFloat = 20
let arcCenter = shapeLayer.position
let radius = shapeLayer.bounds.size.width / 2.0 - offset
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(1.0 * .pi)
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shapeLayer.shadowOffset = CGSize(width: 0, height: 0)
shapeLayer.shadowColor = UIColor.red.cgColor
shapeLayer.shadowOpacity = 1.0
shapeLayer.shadowRadius = 25
shapeLayer.path = circlePath.cgPath
As you can see the shadow surrounds the outer part of the semi-circle shape.
Could anyone give me any hints on adding a drop shadow around the line only ?
Output:
UIView Extension:
extension UIView {
func renderCircle() {
let semiCircleLayer = CAShapeLayer()
let center = CGPoint (x: self.frame.size.width / 2, y: self.frame.size.height / 2)
let circleRadius = self.frame.size.width / 2
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat(Double.pi * 2), endAngle: CGFloat(Double.pi), clockwise: true)
semiCircleLayer.path = circlePath.cgPath
semiCircleLayer.strokeColor = UIColor.red.cgColor
semiCircleLayer.fillColor = UIColor.clear.cgColor
semiCircleLayer.lineWidth = 8
semiCircleLayer.shadowColor = UIColor.red.cgColor
semiCircleLayer.shadowRadius = 25.0
semiCircleLayer.shadowOpacity = 1.0
semiCircleLayer.shadowPath = circlePath.cgPath.copy(strokingWithWidth: 25, lineCap: .round, lineJoin: .miter, miterLimit: 0)
semiCircleLayer.shadowOffset = CGSize(width: 1.0, height: 1.0)
self.layer.addSublayer(semiCircleLayer)
}
}
Usage:
semiCircleView.renderCircle()

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

Swift 3.0 draw circle with same start and end angle results in line

I've written following code to draw a rect with a whole in it:
fileprivate let strokeWidth: CGFloat = 5
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = strokeWidth
shapeLayer.fillColor = UIColor(white: 0.5, alpha: 0.9).cgColor
let p = UIBezierPath(rect: bounds.insetBy(dx: -strokeWidth, dy: -strokeWidth))
let radius = bounds.size.width / 3
p.move(to: CGPoint(x: bounds.midX + radius, y: bounds.midY))
p.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: radius, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: false)
p.close()
shapeLayer.path = p.cgPath
layer.addSublayer(shapeLayer)
The problem here is this line:
p.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: radius, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: false)
Printing out the description of the path:
UIBezierPath: 0x6000000b35c0; MoveTo {-5, -5}, LineTo {380, -5}, LineTo {380, 672}, LineTo {-5, 672}, Close, MoveTo {312.5, 333.5}, LineTo {312.5, 333.49999999999994}, Close
As you can see, the last two entries are lineTo and close, which gives me not the expected result (a full circle), I'll get nothing because the line is too short between 333.5 and 333.4999999.
This problem occurs since switching to Swift 3, in Objective-C this wasn't a problem.
Changing the end angle to 1.9 * Double.pi will also work, no idea why. But the full circle should have 2 * Double.pi.
Any idea or is it a Swift 3 bug?
Try like this:
let bounds = UIScreen.main.bounds
let strokeWidth: CGFloat = 5
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = strokeWidth
shapeLayer.fillColor = UIColor(white: 0.5, alpha: 0.9).cgColor
let p = UIBezierPath(rect: bounds.insetBy(dx: -strokeWidth, dy: -strokeWidth))
let radius = bounds.size.width / 3
p.move(to: CGPoint(x: bounds.midX + radius, y: bounds.midY))
p.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: radius, startAngle: 2 * .pi, endAngle: 0, clockwise: false)
p.close()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 5
shapeLayer.path = p.cgPath
let view = UIView(frame: bounds)
view.backgroundColor = .yellow
view.layer.addSublayer(shapeLayer)
view

custom UIButton with UIBezierPath

I'm making my own custom UIButton.
I subcalssed the UIButton class and drawRect function
this is my code:
override func drawRect(rect: CGRect)
{
let midPoint = CGPointMake(rect.midX, rect.midY)
if !self.selected
{
let redCircle = UIBezierPath()
let redCircleRadius = rect.width - self.whiteCirlceWidth - self.spaceBetweenCirclesWidth
redCircle.addArcWithCenter(CGPointMake(rect.midX, rect.midY), radius: redCircleRadius,
startAngle: 0 , endAngle: 2 * CGFloat(M_PI), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = redCircle.CGPath
shapeLayer.fillColor = UIColor.redColor().CGColor
shapeLayer.strokeColor = UIColor.blackColor().CGColor
shapeLayer.lineWidth = 4.0
self.layer.addSublayer(shapeLayer)
let whiteCircle = UIBezierPath()
whiteCircle.addArcWithCenter(midPoint,
radius: self.bounds.width / 2,
startAngle: 0, endAngle: 2 * CGFloat(M_PI) , clockwise: true)
UIColor.whiteColor().setFill()
whiteCircle.fill()
}
else
{
let whiteCircle = UIBezierPath()
whiteCircle.addArcWithCenter(midPoint,
radius: self.bounds.width / 2,
startAngle: 0, endAngle: 2 * CGFloat(M_PI) , clockwise: true)
UIColor.whiteColor().setFill()
whiteCircle.fill()
let blankCircle = UIBezierPath()
blankCircle.addArcWithCenter(midPoint, radius: rect.width - self.whiteCirlceWidth - self.spaceBetweenCirclesWidth,
startAngle: 0, endAngle: 2 * CGFloat(M_PI), clockwise: true)
UIColor.blackColor().setFill()
blankCircle.fill()
var rect = CGRect(origin: midPoint, size: CGSizeMake(rect.width / 2, rect.width / 2))
rect.origin.x -= rect.origin.x / 2
rect.origin.y -= rect.origin.y / 2
let roundedRect = UIBezierPath(roundedRect: rect, cornerRadius: 3)
UIColor.redColor().setFill()
roundedRect.fill()
}
}
when I click on the button I do this
#IBAction func recordBtnClicked(sender : UIButton)
{
self.recordBtn.selected = !self.recordBtn.selected
self.recordBtn.setNeedsDisplay()
}
but it doesn't change anything!.
I checked in the debugger and it does the right block of code .
I checked two blocks and they do the right drawing
what's wrong?
I figured it out it was because I added a subLayer and it stucks and I didn't remove it ' so I changed the code and didn't used the sublayer.
you can remove the subLayer each time you call drawRect

How to create rounded corners with CAShapeLayer

Is there a way to add rounded corners to a CAShapeLayer? In my case I needed the shape layer to create a dashed border via lineDashPattern.
^ notice how the dashed line is not rounded
The answer is simple. Create a bézier path with rounded corners.
UPDATE for Swift
view.clipsToBounds = true
view.layer.cornerRadius = 10.0
let border = CAShapeLayer()
border.path = UIBezierPath(roundedRect:view.bounds, cornerRadius:10.0).cgPath
border.frame = view.bounds
border.fillColor = nil
border.strokeColor = UIColor.purple.cgColor
border.lineWidth = borderWidth * 2.0 // doubled since half will be clipped
border.lineDashPattern = [15.0]
view.layer.addSublayer(border)
Objective-C
// (This old code assumes this is within a view with a custom property "border".)
self.clipsToBounds = YES;
self.layer.cornerRadius = 10.0;
self.border = [CAShapeLayer layer];
self.border.fillColor = nil;
self.border.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10.0].cgPath;
self.border.frame = self.bounds;
self.border.strokeColor = [UIColor purpleColor].CGColor;
self.border.lineWidth = borderWidth * 2; // double desired width as half will be clipped
self.border.lineDashPattern = #[#15];
[self.layer addSublayer:self.border];
In swift 4 I created a UIView category (UIView+Borders) with the following function:
func borderDash(withRadius cornerRadius: Float, borderWidth: Float, borderColor: UIColor, dashSize: Int) {
let currentFrame = self.bounds
let shapeLayer = CAShapeLayer()
let path = CGMutablePath()
let radius = CGFloat(cornerRadius)
// Points - Eight points that define the round border. Each border is defined by two points.
let topLeftPoint = CGPoint(x: radius, y: 0)
let topRightPoint = CGPoint(x: currentFrame.size.width - radius, y: 0)
let middleRightTopPoint = CGPoint(x: currentFrame.size.width, y: radius)
let middleRightBottomPoint = CGPoint(x: currentFrame.size.width, y: currentFrame.size.height - radius)
let bottomRightPoint = CGPoint(x: currentFrame.size.width - radius, y: currentFrame.size.height)
let bottomLeftPoint = CGPoint(x: radius, y: currentFrame.size.height)
let middleLeftBottomPoint = CGPoint(x: 0, y: currentFrame.size.height - radius)
let middleLeftTopPoint = CGPoint(x: 0, y: radius)
// Points - Four points that are the center of the corners borders.
let cornerTopRightCenter = CGPoint(x: currentFrame.size.width - radius, y: radius)
let cornerBottomRightCenter = CGPoint(x: currentFrame.size.width - radius, y: currentFrame.size.height - radius)
let cornerBottomLeftCenter = CGPoint(x: radius, y: currentFrame.size.height - radius)
let cornerTopLeftCenter = CGPoint(x: radius, y: radius)
// Angles - The corner radius angles.
let topRightStartAngle = CGFloat(Double.pi * 3 / 2)
let topRightEndAngle = CGFloat(0)
let bottomRightStartAngle = CGFloat(0)
let bottmRightEndAngle = CGFloat(Double.pi / 2)
let bottomLeftStartAngle = CGFloat(Double.pi / 2)
let bottomLeftEndAngle = CGFloat(Double.pi)
let topLeftStartAngle = CGFloat(Double.pi)
let topLeftEndAngle = CGFloat(Double.pi * 3 / 2)
// Drawing a border around a view.
path.move(to: topLeftPoint)
path.addLine(to: topRightPoint)
path.addArc(center: cornerTopRightCenter,
radius: radius,
startAngle: topRightStartAngle,
endAngle: topRightEndAngle,
clockwise: false)
path.addLine(to: middleRightBottomPoint)
path.addArc(center: cornerBottomRightCenter,
radius: radius,
startAngle: bottomRightStartAngle,
endAngle: bottmRightEndAngle,
clockwise: false)
path.addLine(to: bottomLeftPoint)
path.addArc(center: cornerBottomLeftCenter,
radius: radius,
startAngle: bottomLeftStartAngle,
endAngle: bottomLeftEndAngle,
clockwise: false)
path.addLine(to: middleLeftTopPoint)
path.addArc(center: cornerTopLeftCenter,
radius: radius,
startAngle: topLeftStartAngle,
endAngle: topLeftEndAngle,
clockwise: false)
// Path is set as the shapeLayer object's path.
shapeLayer.path = path;
shapeLayer.backgroundColor = UIColor.clear.cgColor
shapeLayer.frame = currentFrame
shapeLayer.masksToBounds = false
shapeLayer.setValue(0, forKey: "isCircle")
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = borderColor.cgColor
shapeLayer.lineWidth = CGFloat(borderWidth)
shapeLayer.lineDashPattern = [NSNumber(value: dashSize), NSNumber(value: dashSize)]
shapeLayer.lineCap = kCALineCapRound
self.layer.addSublayer(shapeLayer)
self.layer.cornerRadius = radius;
}

Resources