I have a custom path drawn using CAShapeLayer
in a UIView
. Its a semicircle, upper half, drawn from left to right.
Following is the code used, writted in a UIView
subclass:
func drawSemicircle() {
// drawing an upper half of a circle -> 180 degree to 0 degree, clockwise
let startAngle = CGFloat(M_PI)
let endAngle = CGFloat(0.0)
let centerPoint = CGPointMake(CGRectGetWidth(frame)/2 , CGRectGetHeight(frame))
// path set here
let semirCircleLayer = CAShapeLayer()
let semirCirclePath = UIBezierPath(arcCenter:centerPoint, radius: CGRectGetWidth(frame)/2 - 20.0 , startAngle:startAngle, endAngle:endAngle, clockwise: true)
semirCircleLayer.path = semirCirclePath.CGPath
// layer customisation
semirCircleLayer.fillColor = UIColor.clearColor().CGColor
semirCircleLayer.strokeColor = UIColor(red: 237.0/255.0, green: 236.0/255.0, blue: 236.0/255.0, alpha: 1.0).CGColor
semirCircleLayer.lineWidth = 20.0
semirCircleLayer.lineCap = kCALineCapButt
layer.addSublayer(semirCircleLayer)
}
Current path looks like this:
What I am tryign to implement is to mark some points on the bar .Like a analog clock graduation + diigts, but not so complex. Just mark 1 point at any progress level. So a final output, something like this :
Can I get some help on this?
You'll want to use another CAShapeLayer
for the graduation line and a CATextLayer
for the text at the end of the graduation.
Instead of doing Core Graphics drawing as my answer here does, I recommend you use a 100% layer approach as to best fit in with your existing code.
Something like this should do the trick:
func drawSemicircle() {
// drawing an upper half of a circle -> 180 degree to 0 degree, clockwise
let startAngle = CGFloat(M_PI)
let endAngle = CGFloat(0.0)
let centerPoint = CGPoint(x: CGRectGetWidth(frame)*0.5 , y: CGRectGetHeight(frame))
let radius = frame.size.width*0.5 - 80.0 // radius of your arc
// path set here
let semiCircleLayer = CAShapeLayer()
semiCircleLayer.frame = bounds // requried for layer calculations
let semiCirclePath = UIBezierPath(arcCenter:centerPoint, radius:radius, startAngle:startAngle, endAngle:endAngle, clockwise: true)
semiCircleLayer.path = semiCirclePath.CGPath
// layer customisation
semiCircleLayer.fillColor = UIColor.clearColor().CGColor
semiCircleLayer.strokeColor = UIColor(red: 237.0/255.0, green: 236.0/255.0, blue: 236.0/255.0, alpha: 1.0).CGColor
semiCircleLayer.lineWidth = 20.0
semiCircleLayer.lineCap = kCALineCapButt
layer.addSublayer(semiCircleLayer)
// draw graduation (cue the wall of code!)
let graduationLayer = CAShapeLayer() // the graduation layer that'll display the graduation
graduationLayer.frame = semiCircleLayer.bounds
let graduationWidth = CGFloat(4.0) // the width of the graduation
let graduationLength = CGFloat(50.0) // the length of the graduation
let graduationColor = UIColor.redColor() // the color of both the graduation line and text
let startGradRad = radius-semiCircleLayer.lineWidth*0.5 // the starting radius of the graduation
let endGradRad = startGradRad+graduationLength // the ending radius of the graduation
let graduationAngle = CGFloat(M_PI*0.79) // 21% along the arc from the left (0 degrees coresponds to the right hand side of the circle, with the positive angle direction going anti-clocwise (much like a unit circle in maths), so we define 79% along the arc, from the right hand side)
// the starting point of the graduation line. the angles are negative as the arc is effectively drawn upside-down in the UIKit coordinate system.
let startGradPoint = CGPoint(x: cos(-graduationAngle)*startGradRad+centerPoint.x, y: sin(-graduationAngle)*startGradRad+centerPoint.y)
let endGradPoint = CGPoint(x: cos(-graduationAngle)*endGradRad+centerPoint.x, y: sin(-graduationAngle)*endGradRad+centerPoint.y)
// the path for the graduation line
let graduationPath = UIBezierPath()
graduationPath.moveToPoint(startGradPoint) // start point
graduationPath.addLineToPoint(endGradPoint) // end point
graduationLayer.path = graduationPath.CGPath // add path to the graduation shape layer
// configure stroking options
graduationLayer.fillColor = UIColor.clearColor().CGColor
graduationLayer.strokeColor = graduationColor.CGColor
graduationLayer.lineWidth = graduationWidth
// add to semi-circle layer
semiCircleLayer.addSublayer(graduationLayer)
// the font of the text to render at the end of the graduation
let textFont = UIFont.systemFontOfSize(30)
// the text to render at the end of the graduation - do you custom value logic here
let str : NSString = "value"
// default paragraph style
let paragraphStyle = NSParagraphStyle()
// the text attributes dictionary. used to obtain a size of the drawn text in order to calculate its frame
let textAttributes = [NSParagraphStyleAttributeName:paragraphStyle, NSFontAttributeName:textFont]
// size of the rendered text
let textSize = str.sizeWithAttributes(textAttributes)
let xOffset = abs(cos(graduationAngle))*textSize.width*0.5 // the x-offset of the text from the end of the graduation line
let yOffset = abs(sin(graduationAngle))*textSize.height*0.5 // the y-offset of the text from the end of the graduation line
/// the padding between the graduation line and the text
let graduationTextPadding = CGFloat(5.0)
// bit of pythagorus to determine how far away the center of the text lies from the end of the graduation line. multiplying the values together is cheaper than using pow. the text padding is added onto it.
let textOffset = sqrt(xOffset*xOffset+yOffset*yOffset)+graduationTextPadding
// the center of the text to render
let textCenter = CGPoint(x: cos(-graduationAngle)*textOffset+endGradPoint.x, y: sin(-graduationAngle)*textOffset+endGradPoint.y)
// the frame of the text to render
let textRect = CGRect(x: textCenter.x-textSize.width*0.5, y: textCenter.y-textSize.height*0.5, width: textSize.width, height: textSize.height)
let textLayer = CATextLayer()
textLayer.contentsScale = UIScreen.mainScreen().scale // to ensure the text is rendered at the screen scale
textLayer.frame = textRect
textLayer.string = str
textLayer.font = textFont
textLayer.fontSize = textFont.pointSize // required as CATextLayer ignores the font size of the font you pass
textLayer.foregroundColor = graduationColor.CGColor // color of text
graduationLayer.addSublayer(textLayer)
}
Output:
There's quite a lot of code here, as you have to do some extra logic in order to calculate the size of the CATextLayer
. You could simplify this by using a UILabel
as you can use sizeToFit
in order to calculate the size, however this may complicate the layer hierarchy.
I've tried my best to explain each line of code - but if you still have questions, I'll be happy to answer them!
Furthermore, if you re-structure the code, you could easily allow for the graduation angle, as well as the other variables to be changed from outside the class. I've provided an example of this in the full project below.
Full Project: https://github.com/hamishknight/Dial-View