CATextLayer position and Transformation issue - ios

I am trying to build a control like attached circle image with multiple segment having equal space for each part. Number of segments can change depend upon provided array.
I have developed this so far using CAShapeLayer and UIBezierPath. Also added text in the center of each shape Layer.
I have added my code so far for generating this control.
let circleLayer = CALayer()
func createCircle(_ titleArray:[String]) {
update(bounds: bounds, titleArray: titleArray)
containerView.layer.addSublayer(circleRenderer.circleLayer)
}
func update(bounds: CGRect, titleArray: [String]) {
let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
circleLayer.position = position
circleLayer.bounds = bounds
update(titleArray: titles)
}
func update(titleArray: [String]) {
let center = CGPoint(x: circleLayer.bounds.size.width / 2.0, y: circleLayer.bounds.size.height / 2.0)
let radius:CGFloat = min(circleLayer.bounds.size.width, circleLayer.bounds.size.height) / 2
let segmentSize = CGFloat((Double.pi*2) / Double(titleArray.count))
for i in 0..<titleArray.count {
let startAngle = segmentSize*CGFloat(i) - segmentSize/2
let endAngle = segmentSize*CGFloat(i+1) - segmentSize/2
let midAngle = (startAngle+endAngle)/2
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.random.cgColor
let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
bezierPath.addLine(to: center)
shapeLayer.path = bezierPath.cgPath
let height = titleArray[i].height(withConstrainedWidth: radius-20, font: UIFont.systemFont(ofSize: 15))
let frame = shapeLayer.path?.boundingBoxOfPath ?? CGRect.zero
let textLayer = TextLayer()
textLayer.frame = CGRect(x: 0, y: 0, width: 70, height: height)
textLayer.position = CGPoint(x: frame.center.x, y: frame.center.y)
textLayer.fontSize = 15
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .center
textLayer.string = titleArray[i]
textLayer.isWrapped = true
textLayer.backgroundColor = UIColor.black.cgColor
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.transform = CATransform3DMakeRotation(midAngle, 0.0, 0.0, 1.0)
shapeLayer.addSublayer(textLayer)
circleLayer.addSublayer(shapeLayer)
}
}
circleLayer is added as superlayer, containing full area of UIView.
My requirement is to add text centered vertically and horizontally within shape with angle. I am facing issue with centring text within shape while angle is fine.
Thanks
Edit:
If I remove textLayer rotation code, then It look like this image.

Your labels are not "centered" because you're using the geometric center of the wedge bounding-box:
What you need to do is calculate an "inner" circle, with 1/2 of the full radius, and then find the points on that circle to place your labels.
So, first we calculate the circle:
Then bisect each angle and find the point on the circle:
Then calculate the bounding-box for the label (I used max-width of radius * 0.6), put the center of that frame on the point on the circle, and then rotate the text layer:
And the result, without the "guides":
Note: For these images, I used radius * 0.55 - or just slightly further out than exactly 1/2 of the radius - for the "inner circle". This gave me just slightly better appearance, due to the wedges narrowing as we get to the center of the circle. Changing that to radius * 0.6 might even look better.
Here is the code to generate this view:
struct Wedge {
var color: UIColor = .cyan
var label: String = ""
}
class WedgeView: UIView {
var wedges: [Wedge] = []
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
}
override func layoutSubviews() {
super.layoutSubviews()
layer.sublayers?.forEach { $0.removeFromSuperlayer() }
setup()
}
func setup() -> Void {
// initialize local variables
var startAngle: CGFloat = 0
var outerRadius: CGFloat = 0.0
var halfRadius: CGFloat = 0.0
// initialize local constants
let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let diameter = bounds.width
let fontHeight: CGFloat = ceil(12.0 * (bounds.height / 300.0))
let textLayerFont = UIFont.systemFont(ofSize: fontHeight, weight: .light)
outerRadius = diameter * 0.5
halfRadius = outerRadius * 0.55
let labelMaxWidth:CGFloat = outerRadius * 0.6
startAngle = -(.pi * (1.0 / CGFloat(wedges.count)))
for i in 0..<wedges.count {
let endAngle = startAngle + 2 * .pi * (1.0 / CGFloat(wedges.count))
let shape = CAShapeLayer()
let path: UIBezierPath = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addLine(to: viewCenter)
path.close()
shape.path = path.cgPath
shape.fillColor = wedges[i].color.cgColor
shape.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(shape)
let textLayer = CATextLayer()
textLayer.font = textLayerFont
textLayer.fontSize = fontHeight
let string = wedges[i].label
textLayer.string = string
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.backgroundColor = UIColor.black.cgColor
textLayer.isWrapped = true
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.contentsScale = UIScreen.main.scale
let bisectAngle = startAngle + ((endAngle - startAngle) * 0.5)
let p = CGPoint.pointOnCircle(center: viewCenter, radius: halfRadius, angle: bisectAngle)
var textLayerframe = CGRect(x: 0, y: 0, width: labelMaxWidth, height: 0)
let h = string.getLableHeight(labelMaxWidth, usingFont: textLayerFont)
textLayerframe.size.height = h
textLayerframe.origin.x = p.x - (textLayerframe.size.width * 0.5)
textLayerframe.origin.y = p.y - (textLayerframe.size.height * 0.5)
textLayer.frame = textLayerframe
self.layer.addSublayer(textLayer)
textLayer.transform = CATransform3DMakeRotation(bisectAngle, 0.0, 0.0, 1.0)
// uncomment this block to show the dashed-lines
/*
let biLayer = CAShapeLayer()
let dash = UIBezierPath()
dash.move(to: viewCenter)
dash.addLine(to: p)
biLayer.strokeColor = UIColor.yellow.cgColor
biLayer.lineDashPattern = [4, 4]
biLayer.path = dash.cgPath
self.layer.addSublayer(biLayer)
*/
startAngle = endAngle
}
// uncomment this block to show the half-radius circle
/*
let tempLayer: CAShapeLayer = CAShapeLayer()
tempLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: outerRadius - halfRadius, dy: outerRadius - halfRadius)).cgPath
tempLayer.fillColor = UIColor.clear.cgColor
tempLayer.strokeColor = UIColor.green.cgColor
tempLayer.lineWidth = 1.0
self.layer.addSublayer(tempLayer)
*/
}
}
class WedgesWithRotatedLabelsViewController: UIViewController {
let wedgeView: WedgeView = WedgeView()
var wedges: [Wedge] = []
let colors: [UIColor] = [
UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.50, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.00, blue: 1.00, alpha: 1.0),
UIColor(red: 1.00, green: 0.50, blue: 0.50, alpha: 1.0),
UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
UIColor(red: 0.50, green: 0.50, blue: 1.00, alpha: 1.0),
]
let labels: [String] = [
"This is long text for Label 1",
"Label 2",
"Longer Label 3",
"Label 4",
"Label 5",
"Label 6",
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
for (c, s) in zip(colors, labels) {
wedges.append(Wedge(color: c, label: s))
}
wedgeView.wedges = wedges
view.addSubview(wedgeView)
wedgeView.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
wedgeView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.8),
wedgeView.heightAnchor.constraint(equalTo: wedgeView.widthAnchor),
wedgeView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
wedgeView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
Couple of "helper" extensions used in the above code:
// get a random color
extension UIColor {
static var random: UIColor {
return UIColor(red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1),
alpha: 1.0)
}
}
// get the point on a circle at specific radian
extension CGPoint {
static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
}
// get height of word-wrapping string with max-width
extension String {
func getLableHeight(_ forWidth: CGFloat, usingFont: UIFont) -> CGFloat {
let constraintRect = CGSize(width: forWidth, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: usingFont], context: nil)
return ceil(boundingBox.height)
}
}

Related

How to do hit detection in core graphics

Core graphics is pretty new to me, and I'm facing some issues detecting clicks on my custom graphics.
I generated som code with the demo of paincode which i then heavily modified. It draws a "pie" like this:
The code I used for this looks like this:
import UIKit
public class DrawTest : NSObject {
static var hitAreas = [Int:UIBezierPath]()
static func didHit(_ point: CGPoint){
let res = hitAreas.first{ $0.value.contains(point) }?.key
print("HIT: ", res)
}
public class func drawDartboard(frame targetFrame: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
let sliceRect = CGRect(x: 0, y: 0, width: 100, height: 100)
context.saveGState()
context.clip(to: sliceRect)
context.translateBy(x: sliceRect.minX, y: sliceRect.minY)
context.translateBy(x: 0, y: sliceRect.height)
context.scaleBy(x: 1, y: -1)
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
var slice = 0
while slice < 20 {
let sliceColor = slice%2 == 0 ? dark : light
DrawTest.drawSlice(frame: CGRect(origin: .zero, size: sliceRect.size), roration: CGFloat(slice*18), sliceColor: sliceColor, slice: slice )
slice += 1
}
context.restoreGState()
}
public class func drawSlice(frame targetFrame: CGRect, roration: CGFloat, sliceColor: UIColor, slice: Int) {
let context = UIGraphicsGetCurrentContext()!
context.saveGState()
let resizedFrame: CGRect = targetFrame
context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
context.scaleBy(x: resizedFrame.width / 100, y: resizedFrame.height / 100)
context.saveGState()
context.translateBy(x: 49.99, y: 50)
context.rotate(by: roration * CGFloat.pi/180)
let sliceFillPath = UIBezierPath()
sliceFillPath.move(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.addCurve(to: CGPoint(x: 7.83, y: 49.38), controlPoint1: CGPoint(x: -2.63, y: 50.2), controlPoint2: CGPoint(x: 2.65, y: 50.2))
sliceFillPath.addLine(to: CGPoint(x: 0.01, y: -0))
sliceFillPath.addLine(to: CGPoint(x: -7.82, y: 49.38))
sliceFillPath.close()
sliceColor.setFill()
sliceFillPath.fill()
hitAreas[slice] = sliceFillPath
context.restoreGState()
}
}
I'm calling the draw code from a simple UIView subclass like below. This is also were I attach a TapGerstureRecognizer.
import UIKit
class DartBoardView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
DrawTest.didHit(loc)
}
}
override func draw(_ rect: CGRect) {
DrawTest.drawDartboard(frame: bounds)
}
}
The drawing looks like I want it to, but I want to be able to select each of the slices, this is the part that is not working. I am pretty sure that the issue has to do with the point I pass to didHit is local to my View but the UIBezierPath I store in hitAreas and call contains uses the local coordinates of the UIBezierPath, this is why I never get a hit.
I have no idea how to solve this and desperately need help. My guess is that this should be solved by 1) drawing my slices directy on the UIView´s coordinate system, but that would require a lot af math 2) somehow translate the local coordinates of each UIBezierPath to the scope of the view when hit testing
This is all very confusing at all constructive input is very appreciated.
There are various approaches, depending on exactly what your end-goal is.
One approach:
calculate the "degrees-per-slice" ... 360 / 20 = 18
get the angle from the center point to the touch point
"fix" the angle by 1/2 of the slice width (since the slices don't start at zero)
divide that angle by degrees-per-slice to get the slice number
Use these two extensions to make it easy to get the angle (in degrees):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
extension CGPoint {
func angle(to otherPoint: CGPoint) -> CGFloat {
let pX = otherPoint.x - x
let pY = otherPoint.y - y
let radians = atan2f(Float(pY), Float(pX))
var degrees = CGFloat(radians).degrees
while degrees < 0 {
degrees += 360
}
return degrees
}
}
And, in the code you posted, in your DrawTest class, change didHit to:
static func didHit(_ point: CGPoint, in bounds: CGRect){
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let angle = c.angle(to: point)
var fixedAngle = Int(angle) + 99 // 90 degrees + 1/2 of slice width
if fixedAngle >= 360 {
fixedAngle -= 360
}
print("HIT:", fixedAngle / 18)
}
and include the bounds when you call it from DartBoardView class as:
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
// include self's bounds
DrawTest.didHit(loc, in: bounds)
}
}
Drawbacks include:
you'd also need to check the "line length" to make sure it doesn't extend outside the circle
you don't have easy access to the slice bezier paths (if you want to do something else with them)
Another approach would be to use shape layers for each slice, making it easier to track the bezier paths.
Start with a Struct for the slices:
struct Slice {
var color: UIColor = .white
var path: UIBezierPath = UIBezierPath()
var shapeLayer: CAShapeLayer = CAShapeLayer()
var key: Int = 0
}
The DartBoardView class becomes (note: it uses the same CGFloat extension from above):
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
class DartBoardView: UIView {
// array of slices
var slices: [Slice] = []
// slice width in degrees
let sliceWidth: CGFloat = 360.0 / 20.0
// easy to understand 12 o'clock (3 o'clock is Zero)
let twelveOClock: CGFloat = 270
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
let dark = UIColor(red: 0.235, green: 0.208, blue: 0.208, alpha: 1.000)
let light = UIColor(red: 0.435, green: 0.408, blue: 0.408, alpha: 1.000)
for slice in 0..<20 {
let sliceColor = slice % 2 == 1 ? dark : light
let s = Slice(color: sliceColor, key: slice)
s.shapeLayer.fillColor = s.color.cgColor
layer.addSublayer(s.shapeLayer)
slices.append(s)
}
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = slices.first(where: { $0.path.contains(loc) }) {
print("HIT:", s.key)
} else {
print("Tapped outside the circle!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
let c: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let radius: CGFloat = bounds.midX
// slice width in radians
let ww: CGFloat = sliceWidth.radians
// start 1/2 sliceWidth less than 12 o'clock
var startDegrees: CGFloat = twelveOClock.radians - (ww * 0.5)
for i in 0..<slices.count {
let endDegrees: CGFloat = startDegrees + ww
let pth: UIBezierPath = UIBezierPath()
pth.addArc(withCenter: c, radius: radius, startAngle: startDegrees, endAngle: endDegrees, clockwise: true)
pth.addLine(to: c)
pth.close()
slices[i].path = pth
slices[i].shapeLayer.path = pth.cgPath
startDegrees = endDegrees
}
}
}
And here's an example controller class to demonstrate:
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .black
}
}
Edit
Not as complex as it may seem.
Here's an implementation of a full Dart Board (without the numbers - I'll leave that as an exercise for you):
Segment Struct
struct Segment {
var value: Int = 0
var multiplier: Int = 1
var color: UIColor = .cyan
var path: UIBezierPath = UIBezierPath()
var layer: CAShapeLayer = CAShapeLayer()
}
DartBoardView class
class DartBoardView: UIView {
var doubleSegments: [Segment] = [Segment]()
var outerSingleSegments: [Segment] = [Segment]()
var tripleSegments: [Segment] = [Segment]()
var innerSingleSegments: [Segment] = [Segment]()
var singleBullSegment: Segment = Segment()
var doubleBullSegment: Segment = Segment()
var allSegments: [Segment] = [Segment]()
let boardLayer: CAShapeLayer = CAShapeLayer()
let darkColor: UIColor = UIColor(white: 0.1, alpha: 1.0)
let lightColor: UIColor = UIColor(red: 0.975, green: 0.9, blue: 0.8, alpha: 1.0)
let darkRedColor: UIColor = UIColor(red: 0.8, green: 0.1, blue: 0.1, alpha: 1.0)
let darkGreenColor: UIColor = UIColor(red: 0.0, green: 0.5, blue: 0.3, alpha: 1.0)
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(boardLayer)
boardLayer.fillColor = UIColor.black.cgColor
// points starting at 3 o'clock
let values: [Int] = [
6, 10, 15, 2, 17, 3, 19, 7, 16, 8, 11, 14, 9, 12, 5, 20, 1, 18, 4, 13,
]
// local vars for reuse
var seg: Segment = Segment()
var c: UIColor = .white
// doubles and triples
for i in 0..<values.count {
c = i % 2 == 1 ? darkRedColor : darkGreenColor
seg = Segment(value: values[i],
multiplier: 2,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 3,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
tripleSegments.append(seg)
}
// singles
for i in 0..<values.count {
c = i % 2 == 1 ? darkColor : lightColor
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
outerSingleSegments.append(seg)
seg = Segment(value: values[i],
multiplier: 1,
color: c,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
innerSingleSegments.append(seg)
}
// bull and double bull
seg = Segment(value: 25,
multiplier: 1,
color: darkGreenColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
singleBullSegment = seg
seg = Segment(value: 25,
multiplier: 2,
color: darkRedColor,
layer: CAShapeLayer())
layer.addSublayer(seg.layer)
doubleBullSegment = seg
let gesture = UITapGestureRecognizer(target: self, action: #selector(self.clickAction(sender:)))
addGestureRecognizer(gesture)
}
#objc
func clickAction(sender : UITapGestureRecognizer) {
if sender.state == .recognized
{
let loc = sender.location(in: self)
if let s = allSegments.first(where: { $0.path.contains(loc) }) {
print("HIT:", s.multiplier == 3 ? "Triple" : s.multiplier == 2 ? "Double" : "Single", s.value)
} else {
print("Tapped outside!")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
// initialize local variables for reuse / readability
var startAngle: CGFloat = 0
var outerDoubleRadius: CGFloat = 0.0
var innerDoubleRadius: CGFloat = 0.0
var outerTripleRadius: CGFloat = 0.0
var innerTripleRadius: CGFloat = 0.0
var outerBullRadius: CGFloat = 0.0
var innerBullRadius: CGFloat = 0.0
// initialize local constants
let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
// leave 20% for the numbers area
let diameter = bounds.width * 0.8
// dart board radii in mm
let specRadii: [CGFloat] = [
170, 162, 107, 99, 16, 6
]
// convert to view size
let factor: CGFloat = (diameter * 0.5) / specRadii[0]
outerDoubleRadius = specRadii[0] * factor
innerDoubleRadius = specRadii[1] * factor
outerTripleRadius = specRadii[2] * factor
innerTripleRadius = specRadii[3] * factor
outerBullRadius = specRadii[4] * factor
innerBullRadius = specRadii[5] * factor
let wireColor: UIColor = UIColor(white: 0.8, alpha: 1.0)
let wedgeWidth: CGFloat = 360.0 / 20.0
let incAngle: CGFloat = wedgeWidth.radians
startAngle = -(incAngle * 0.5)
var path: UIBezierPath = UIBezierPath()
// outer board layer
path = UIBezierPath(ovalIn: bounds)
boardLayer.path = path.cgPath
for i in 0..<20 {
let endAngle = startAngle + incAngle
var shape = doubleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
doubleSegments[i].path = path
shape.fillColor = doubleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = outerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerDoubleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
outerSingleSegments[i].path = path
shape.fillColor = outerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = tripleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
tripleSegments[i].path = path
shape.fillColor = tripleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = innerSingleSegments[i].layer
path = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: innerTripleRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: viewCenter, radius: outerBullRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shape.path = path.cgPath
innerSingleSegments[i].path = path
shape.fillColor = innerSingleSegments[i].color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
startAngle = endAngle
}
let singleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - outerBullRadius, y: viewCenter.y - outerBullRadius, width: outerBullRadius * 2, height: outerBullRadius * 2))
let doubleBullPath = UIBezierPath(ovalIn: CGRect(x: viewCenter.x - innerBullRadius, y: viewCenter.y - innerBullRadius, width: innerBullRadius * 2, height: innerBullRadius * 2))
var shape = singleBullSegment.layer
singleBullPath.append(doubleBullPath)
singleBullPath.usesEvenOddFillRule = true
shape.fillRule = .evenOdd
shape.path = singleBullPath.cgPath
singleBullSegment.path = singleBullPath
shape.fillColor = singleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
shape = doubleBullSegment.layer
shape.path = doubleBullPath.cgPath
doubleBullSegment.path = doubleBullPath
shape.fillColor = doubleBullSegment.color.cgColor
shape.strokeColor = wireColor.cgColor
shape.borderWidth = 1.0
shape.borderColor = wireColor.cgColor
// append all segments for hit-testing
allSegments = []
allSegments.append(contentsOf: tripleSegments)
allSegments.append(contentsOf: outerSingleSegments)
allSegments.append(contentsOf: doubleSegments)
allSegments.append(contentsOf: innerSingleSegments)
allSegments.append(singleBullSegment)
allSegments.append(doubleBullSegment)
}
}
CGFloat extension
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180) / .pi
}
var radians: CGFloat {
return self * .pi / 180.0
}
}
Example view controller
class DartBoardViewController: UIViewController {
let dartBoard = DartBoardView()
override func viewDidLoad() {
super.viewDidLoad()
dartBoard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(dartBoard)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
dartBoard.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
dartBoard.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
dartBoard.heightAnchor.constraint(equalTo: dartBoard.widthAnchor),
dartBoard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
dartBoard.backgroundColor = .clear
}
}
Result:
and debug output from a few taps:
HIT: Double 20
HIT: Single 18
HIT: Triple 2
HIT: Single 25
HIT: Double 25

How to add shadows to gradient border layer . refer the below image

How to add a shadow effect to this gradient border.
Here is the sample extension to create a border layer with a specified width. When I tried to add a shadow layer whole UI gets affected.
self.gradientBorder(width: 3, colors: UIColor.defaultGradient, andRoundCornersWithRadius: min(bounds.size.height, bounds.size.width))
extension UIView {
private static let kLayerNameGradientBorder = "GradientBorderLayer"
func gradientBorder(width: CGFloat,
colors: [UIColor],
startPoint: CGPoint = CGPoint(x: 1.0, y: 0.0),
endPoint: CGPoint = CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius cornerRadius: CGFloat = 0) {
let existingBorder = gradientBorderLayer()
let border = existingBorder ?? CAGradientLayer()
border.frame = CGRect(x: bounds.origin.x, y: bounds.origin.y,
width: bounds.size.width + width, height: bounds.size.height + width)
border.colors = colors.map { $0.cgColor }
border.startPoint = startPoint
border.endPoint = endPoint
let mask = CAShapeLayer()
let maskRect = CGRect(x: bounds.origin.x + width/2, y: bounds.origin.y + width/2,
width: bounds.size.width - width, height: bounds.size.height - width)
let path = UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath
mask.path = path
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.black.cgColor
mask.backgroundColor = UIColor.black.cgColor
mask.lineWidth = width
mask.masksToBounds = false
border.mask = mask
let exists = (existingBorder != nil)
if !exists {
layer.addSublayer(border)
}
}
private func gradientBorderLayer() -> CAGradientLayer? {
let borderLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameGradientBorder }
if borderLayers?.count ?? 0 > 1 {
fatalError()
}
return borderLayers?.first as? CAGradientLayer
}
}
Edit
Minor changes from initial code:
background layer doesn't interfere with added subviews
handles resizing correctly (when called in viewDidLayoutSubviews)
You can do this by adding a shadow properties to the view's layer, and adding another layer as a "background" layer.
After Edit... Here is your UIView extension - slightly modified (see the comments):
extension UIView {
private static let kLayerNameGradientBorder = "GradientBorderLayer"
private static let kLayerNameBackgroundLayer = "BackgroundLayer"
func gradientBorder(width: CGFloat,
colors: [UIColor],
startPoint: CGPoint = CGPoint(x: 1.0, y: 0.0),
endPoint: CGPoint = CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius cornerRadius: CGFloat = 0,
bgColor: UIColor = .white,
shadowColor: UIColor = .black,
shadowRadius: CGFloat = 5.0,
shadowOpacity: Float = 0.75,
shadowOffset: CGSize = CGSize(width: 0.0, height: 0.0)
) {
let existingBackground = backgroundLayer()
let bgLayer = existingBackground ?? CALayer()
bgLayer.name = UIView.kLayerNameBackgroundLayer
// set its color
bgLayer.backgroundColor = bgColor.cgColor
// insert at 0 to not cover other layers
if existingBackground == nil {
layer.insertSublayer(bgLayer, at: 0)
}
// use same cornerRadius as border
bgLayer.cornerRadius = cornerRadius
// inset its frame by 1/2 the border width
bgLayer.frame = bounds.insetBy(dx: width * 0.5, dy: width * 0.5)
// set shadow properties
layer.shadowColor = shadowColor.cgColor
layer.shadowRadius = shadowRadius
layer.shadowOpacity = shadowOpacity
layer.shadowOffset = shadowOffset
let existingBorder = gradientBorderLayer()
let border = existingBorder ?? CAGradientLayer()
border.name = UIView.kLayerNameGradientBorder
// don't do this
// border.frame = CGRect(x: bounds.origin.x, y: bounds.origin.y,
// width: bounds.size.width + width, height: bounds.size.height + width)
// use this instead
border.frame = bounds
border.colors = colors.map { $0.cgColor }
border.startPoint = startPoint
border.endPoint = endPoint
let mask = CAShapeLayer()
let maskRect = CGRect(x: bounds.origin.x + width/2, y: bounds.origin.y + width/2,
width: bounds.size.width - width, height: bounds.size.height - width)
let path = UIBezierPath(roundedRect: maskRect, cornerRadius: cornerRadius).cgPath
mask.path = path
mask.fillColor = UIColor.clear.cgColor
mask.strokeColor = UIColor.black.cgColor
mask.backgroundColor = UIColor.black.cgColor
mask.lineWidth = width
mask.masksToBounds = false
border.mask = mask
let exists = (existingBorder != nil)
if !exists {
layer.addSublayer(border)
}
}
private func backgroundLayer() -> CALayer? {
let aLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameBackgroundLayer }
if aLayers?.count ?? 0 > 1 {
fatalError()
}
return aLayers?.first
}
private func gradientBorderLayer() -> CAGradientLayer? {
let borderLayers = layer.sublayers?.filter { return $0.name == UIView.kLayerNameGradientBorder }
if borderLayers?.count ?? 0 > 1 {
fatalError()
}
return borderLayers?.first as? CAGradientLayer
}
}
After Edit... and here's an example in-use:
class GradBorderViewController: UIViewController {
var topGradView: UIView = UIView()
// make bottom grad view a button
var botGradView: UIButton = UIButton()
var topBkgView: UIView = UIView()
var botBkgView: UIView = UIView()
let embededLabel: UILabel = UILabel()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
embededLabel.textColor = .red
embededLabel.textAlignment = .center
embededLabel.text = "Label as subview"
botGradView.setTitle("Button", for: [])
botGradView.setTitleColor(.red, for: [])
botGradView.setTitleColor(.lightGray, for: .highlighted)
topGradView.backgroundColor = .clear
botGradView.backgroundColor = .clear
topBkgView.backgroundColor = .yellow
botBkgView.backgroundColor = UIColor(red: 0.5, green: 0.0, blue: 0.0, alpha: 1.0)
[topBkgView, topGradView, botBkgView, botGradView].forEach {
$0.translatesAutoresizingMaskIntoConstraints = false
view.addSubview($0)
}
embededLabel.translatesAutoresizingMaskIntoConstraints = false
// embed label in topGradView
topGradView.addSubview(embededLabel)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// yellow background view on top half, dark-red background view on bottom half
topBkgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
topBkgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
botBkgView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
botBkgView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
topBkgView.topAnchor.constraint(equalTo: g.topAnchor),
botBkgView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
topBkgView.heightAnchor.constraint(equalTo: g.heightAnchor, multiplier: 0.5),
botBkgView.topAnchor.constraint(equalTo: topBkgView.bottomAnchor),
// each grad border view 75% of width, 80-pt constant height
topGradView.widthAnchor.constraint(equalTo: topBkgView.widthAnchor, multiplier: 0.75),
topGradView.heightAnchor.constraint(equalToConstant: 80.0),
botGradView.widthAnchor.constraint(equalTo: topGradView.widthAnchor),
botGradView.heightAnchor.constraint(equalTo: topGradView.heightAnchor),
// center each grad border view in a background view
topGradView.centerXAnchor.constraint(equalTo: topBkgView.centerXAnchor),
topGradView.centerYAnchor.constraint(equalTo: topBkgView.centerYAnchor),
botGradView.centerXAnchor.constraint(equalTo: botBkgView.centerXAnchor),
botGradView.centerYAnchor.constraint(equalTo: botBkgView.centerYAnchor),
// center the embedded label in the topGradView
embededLabel.centerXAnchor.constraint(equalTo: topGradView.centerXAnchor),
embededLabel.centerYAnchor.constraint(equalTo: topGradView.centerYAnchor),
])
botGradView.addTarget(self, action: #selector(self.testTap(_:)), for: .touchUpInside)
}
#objc func testTap(_ sender: Any?) -> Void {
print("Tapped!")
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let a1: [CGFloat] = [173, 97, 222].map({$0 / 255.0})
let a2: [CGFloat] = [0, 198, 182].map({$0 / 255.0})
let c1 = UIColor(red: a1[0], green: a1[1], blue: a1[2], alpha: 1.0)
let c2 = UIColor(red: a2[0], green: a2[1], blue: a2[2], alpha: 1.0)
topGradView.gradientBorder(width: 6,
colors: [c1, c2],
startPoint: CGPoint(x: 0.0, y: 0.0),
endPoint: CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius: topGradView.frame.height * 0.5
)
botGradView.gradientBorder(width: 6,
colors: [c1, c2],
startPoint: CGPoint(x: 0.0, y: 0.0),
endPoint: CGPoint(x: 1.0, y: 1.0),
andRoundCornersWithRadius: topGradView.frame.height * 0.5,
shadowColor: .white,
shadowRadius: 12,
shadowOpacity: 0.95,
shadowOffset: CGSize(width: 0.0, height: 0.0)
)
}
}
After Edit... Results:

How do I get the coordinates from CAShapeLayer

So I am trying to make a progress bar. So I have made circular path, but I want the dot to be at the end of the progress bar, but how do I get the position of the dot to be att the end of the current progress?
private func simpleShape() {
let width: CGFloat = 10
createCircle()
//make circle transparant in middle
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = UIColor.blue.cgColor
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = width
progressLayer.strokeStart = 0
progressLayer.strokeEnd = 0
//unfilled
backLayer.lineWidth = width
backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
backLayer.strokeEnd = 1
self.layer.addSublayer(gradientLayer)
}
private func createCircle() {
//create circle
let circle = UIView(frame: bounds)
circle.layoutIfNeeded()
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)
//add layers
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
circle.layer.addSublayer(backLayer)
circle.layer.addSublayer(progressLayer)
addSubview(circle)
circle.layer.addSublayer(dotLayer)
}
let dotLayer = CAShapeLayer()
public func setProgress(_ progress: CGFloat) {
progressLayer.strokeEnd = CGFloat(progress)
if let progressEndpoint = progressLayer.path?.currentPoint {
dotLayer.position = progressEndpoint
}
}
This is what I'm getting
This is what I want
You’re going to have to calculate it yourself. So figure out the angle from the start and end angles for your arcs:
let angle = (endAngle - startAngle) * progress + startAngle
And then use basic trigonometry to determine where that point falls:
let point = CGPoint(x: centerPoint.x + radius * cos(angle),
y: centerPoint.y + radius * sin(angle))
dotLayer.position = point
By the way, I’d suggest separating the adding of the sublayers (which is part of the initial configuration process) from the updating paths (which is part of the view layout process, which may be called again if the frame of the view changes, constraints are applied, etc). Thus, perhaps:
#IBDesignable
class ProgressView: UIView {
var progress: CGFloat = 0 { didSet { updateProgress() } }
private var centerPoint: CGPoint = .zero
private var radius: CGFloat = 0
private let startAngle: CGFloat = -0.475 * .pi
private let endAngle: CGFloat = 1.525 * .pi
private let lineWidth: CGFloat = 10
private lazy var progressLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineCap = .round
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeStart = 0
shapeLayer.strokeEnd = progress
return shapeLayer
}()
private lazy var backLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = lineWidth
shapeLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1).cgColor
return shapeLayer
}()
private lazy var dotLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.path = UIBezierPath(arcCenter: .zero, radius: lineWidth / 2 * 1.75, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath
shapeLayer.fillColor = UIColor.white.withAlphaComponent(0.5).cgColor
return shapeLayer
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSublayers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
addSublayers()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePaths()
updateProgress()
}
}
private extension ProgressView {
func addSublayers() {
layer.addSublayer(backLayer)
layer.addSublayer(progressLayer)
layer.addSublayer(dotLayer)
}
func updatePaths() {
centerPoint = CGPoint(x: bounds.midX, y: bounds.midY)
radius = min(bounds.width, bounds.height) / 2 * 0.83
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
}
func updateProgress() {
progressLayer.strokeEnd = progress
let angle = (endAngle - startAngle) * progress + startAngle
let point = CGPoint(x: centerPoint.x + radius * cos(angle),
y: centerPoint.y + radius * sin(angle))
dotLayer.position = point
}
}
What you need is rotation Animation
let progressLayer = CAShapeLayer()
let backLayer = CAShapeLayer()
private func simpleShape() {
let width: CGFloat = 15
createCircle()
//make circle transparant in middle
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = #colorLiteral(red: 0.888897419, green: 0.5411034822, blue: 0.04008810222, alpha: 1)
progressLayer.lineCap = CAShapeLayerLineCap.round
progressLayer.lineWidth = width
progressLayer.strokeStart = 0
progressLayer.strokeEnd = 0
//unfilled
backLayer.lineWidth = width
backLayer.strokeColor = #colorLiteral(red: 0.1411764706, green: 0.1725490196, blue: 0.2431372549, alpha: 1)
backLayer.strokeEnd = 1
// self.layer.addSublayer(gradientLayer)
}
private func createCircle() {
//create circle
let circle = UIView(frame: bounds)
let centerPoint = CGPoint (x: circle.bounds.width / 2, y: circle.bounds.width / 2)
let circleRadius: CGFloat = circle.bounds.width / 2 * 0.83
let distance = circle.bounds.width / 2 * 0.17
let circlePath = UIBezierPath(arcCenter: centerPoint, radius: circleRadius, startAngle: CGFloat(-0.475 * Double.pi), endAngle: CGFloat(1.525 * Double.pi), clockwise: true)
//add layers
progressLayer.path = circlePath.cgPath
backLayer.path = circlePath.cgPath
circle.layer.addSublayer(backLayer)
circle.layer.addSublayer(progressLayer)
addSubview(circle)
let circleCenter = CGPoint(x:centerPoint.x - distance,y:centerPoint.y - circleRadius )
let dotCircle = UIBezierPath()
dotCircle.addArc(withCenter:circleCenter, radius: 3, startAngle: CGFloat(-90).deg2rad(), endAngle: CGFloat(270).deg2rad(), clockwise: true)
dotLayer.path = dotCircle.cgPath
dotLayer.position = CGPoint(x:centerPoint.x,y:centerPoint.y )
dotLayer.strokeColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 0.6496753961)
dotLayer.lineWidth = 10
dotLayer.fillColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
dotLayer.isHidden = true
circle.layer.addSublayer(dotLayer)
}
let dotLayer = CAShapeLayer()
public func setProgress(_ progress: CGFloat) {
print(progress)
// progressLayer.strokeEnd = progress
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.beginTime = CACurrentMediaTime() + 0.5;
animation.fromValue = 0
animation.toValue = progress
animation.duration = 2
animation.autoreverses = false
animation.repeatCount = .nan
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
progressLayer.add(animation, forKey: "line")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.dotLayer.isHidden = false
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
// rotateAnimation.beginTime = CACurrentMediaTime() + 0.5;
rotateAnimation.fromValue = (CGFloat( -90)).deg2rad()
rotateAnimation.toValue = (360*progress - 98).deg2rad()
rotateAnimation.duration = 2
rotateAnimation.fillMode = .forwards
rotateAnimation.isRemovedOnCompletion = false
self.dotLayer.add(rotateAnimation, forKey: nil)
}
}

How to skip an area (cutout) when drawing with core graphics

I want to draw some items but leave a true alpha transparency cutout for a circular area. What I want to achieve:
Yellow is example background to show bleed through.
The cutout width is actually wider than the arc stroke, so they don't fully intersect. I need true cutout because I a saving to an image with transparency.
I thought maybe I could use setBlendMode() but I believe that would only work if I wanted my cutout to be exactly the same width as the arc. But there is the gist of how I was trying to go about it:
A Swift workbook follows. Any tips on achieving this are greatly appreciated.
import Foundation
import UIKit
var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
class DonutView : UIView
{
override func draw(_ rect: CGRect)
{
// cutout
let cutoutColor = UIColor(red: 1, green: 0, blue: 0, alpha: 1)
cutoutColor.setFill()
let cutoutPath = UIBezierPath(ovalIn: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
cutoutPath.fill()
// let context = UIGraphicsGetCurrentContext()!
// context.setBlendMode(.sourceOut)
let ringOffset = cutoutWidth/2;
let circleWidth = dimen - ringOffset*2;
// ring
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
ringColor.setStroke()
ringPath.lineWidth = strokeWidth
ringPath.stroke()
// arc
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
arcColor.setStroke()
arcPath.lineWidth = strokeWidth
arcPath.stroke()
}
}
var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow
// View these elements
view
(Edit: I should have stated this initially: this is to ultimately create a UIImage for WatchKit)
With help from How to clear circle in CGContext in iOS
import Foundation
import UIKit
import PlaygroundSupport
var dimen: CGFloat = 200.0;
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
class DonutView : UIImageView
{
override init(frame: CGRect) {
super.init(frame: frame)
UIGraphicsBeginImageContextWithOptions(CGSize(width: dimen, height: dimen), false, 1)
let context = UIGraphicsGetCurrentContext()!
let ringOffset = cutoutWidth/2;
let circleWidth = dimen - ringOffset*2;
// ring
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
let ringColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.3)
ringColor.setStroke()
ringPath.lineWidth = strokeWidth
ringPath.stroke()
// arc
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
let arcColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
arcColor.setStroke()
arcPath.lineWidth = strokeWidth
arcPath.stroke()
// Cutout circle
context.setFillColor(UIColor.clear.cgColor)
context.setBlendMode(.clear)
context.addEllipse(in: CGRect(x: dimen-cutoutWidth, y: dimen/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
context.drawPath(using: .fill)
context.setBlendMode(.normal)
image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
var view = DonutView(frame: CGRect.init(x: 0, y: 0, width: dimen, height: dimen))
view.backgroundColor = UIColor.yellow
// View these elements
view
You can do this by using another CAShapeLayer as a mask.
The portion(s) of the mask layer that are alpha = 1.0 will be fully transparent.
So...
If we make the Arc Layer a sublayer of the Ring Layer, we can then apply the Cutout Layer as a mask, resulting in:
Here is source for a Playground page:
class MyDonutView : UIView
{
let ringLayer = CAShapeLayer()
let arcLayer = CAShapeLayer()
let cutoutLayer = CAShapeLayer()
var strokeWidth: CGFloat = 20.0;
var cutoutWidth: CGFloat = 30.0;
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add arcLayer as a sublayer of ringLayer
ringLayer.addSublayer(arcLayer)
// add ringLayer as a sublayer of self.layer
layer.addSublayer(ringLayer)
// ring layer stroke is black at 0.3 alpha, fill is clear
ringLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.3).cgColor
ringLayer.fillColor = UIColor.clear.cgColor
ringLayer.lineWidth = strokeWidth
// arc layer stroke is black at 0.6 alpha, fill is clear
arcLayer.strokeColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.6).cgColor
arcLayer.lineWidth = strokeWidth
arcLayer.fillColor = UIColor.clear.cgColor
// cutout layer stroke is black (although we're using Zero line width
// fill is black
cutoutLayer.strokeColor = UIColor.red.cgColor
cutoutLayer.lineWidth = 0
cutoutLayer.fillColor = UIColor.red.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// define the "padding" around the ring
let ringOffset = cutoutWidth / 2.0
// define the diameter of the ring
let circleWidth = bounds.size.width - cutoutWidth;
// ring path
let ringPath = UIBezierPath(ovalIn: CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth))
// arc path
let arcRect = CGRect(x: ringOffset, y: ringOffset, width: circleWidth, height: circleWidth)
let arcPath = UIBezierPath()
arcPath.addArc(withCenter: CGPoint(x: arcRect.midX, y: arcRect.midY), radius: arcRect.width / 2, startAngle: -90 * CGFloat.pi/180, endAngle: 37 * CGFloat.pi/180, clockwise: true)
// set ring layer path
ringLayer.path = ringPath.cgPath
// set arc layer path
arcLayer.path = arcPath.cgPath
// create a rect path the full size of bounds of self
let fullPath = UIBezierPath(rect: bounds)
// create a cutout path (the small circle to cut-out of the ring/arc)
let cutoutPath = UIBezierPath(ovalIn: CGRect(x: bounds.size.width-cutoutWidth, y: bounds.size.width/2-cutoutWidth/2, width: cutoutWidth, height: cutoutWidth))
// append the cutout path to the full rect path
fullPath.append(cutoutPath)
// even-odd winding rule
cutoutLayer.fillRule = CAShapeLayerFillRule.evenOdd
// set cutout layer path
cutoutLayer.path = fullPath.cgPath
// use cutout layer to mask ring layer
ringLayer.mask = cutoutLayer
}
}
class TestViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
// instantiate a MyDonutView
let myDonutView = MyDonutView()
// we can set the stroke and cutout widths here
myDonutView.strokeWidth = 20.0
myDonutView.cutoutWidth = 30.0
// we're using auto-layout
myDonutView.translatesAutoresizingMaskIntoConstraints = false
// background color yellow to see the frame
//myDonutView.backgroundColor = .yellow
// otherwise, it should be clear
myDonutView.backgroundColor = .clear
// add as subview
view.addSubview(myDonutView)
// constrain centerX and centerY
// width = 200, height = width
NSLayoutConstraint.activate([
myDonutView.widthAnchor.constraint(equalToConstant: 200.0),
myDonutView.heightAnchor.constraint(equalTo: myDonutView.widthAnchor),
myDonutView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myDonutView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
let vc = TestViewController()
PlaygroundPage.current.liveView = vc

UIBezierPath can't change the color of the line

This is my code
func drawSmallCircles(){
for oneArray in points {
let startAngleRadiant: CGFloat = degreesToRadians(Double(oneArray[0]))
let endAngleRadiant: CGFloat = degreesToRadians(Double(oneArray[1]))
let radius: CGFloat = 100.0
let path = UIBezierPath(arcCenter: center,
radius: radius,
startAngle: startAngleRadiant,
endAngle: endAngleRadiant,
clockwise: true)
let color = UIColor(red: CGFloat(55), green: CGFloat(37), blue: CGFloat(23), alpha: CGFloat(1))
path.fill()
color.setFill()
path.lineWidth = CGFloat(10)
path.stroke()
setNeedsDisplay()
}
}
as you see, i am having a custom color. my problem is that the color always black.
see
override func drawRect(rect: CGRect) {
self.opaque = false;
drawSmallCircles()
}
this is the drawRec for me
Update1
after Rob answer, this is my code
let points = [[270, 290],[300, 320],[330, 350],[0, 20],[30, 50],[60, 80],[90, 110],[120, 140],[150, 170],[180, 200],[210, 230],[240, 260]]
func drawSmallCircles(){
for oneArray in points {
let startAngleRadiant: CGFloat = degreesToRadians(Double(oneArray[0]))
let endAngleRadiant: CGFloat = degreesToRadians(Double(oneArray[1]))
let radius: CGFloat = 50.0
let path = UIBezierPath(arcCenter: CGPoint(x: CGFloat(100), y: CGFloat(100)),
radius: radius,
startAngle: startAngleRadiant,
endAngle: endAngleRadiant,
clockwise: true)
let color = UIColor(red: CGFloat(55/255), green: CGFloat(37/255), blue: CGFloat(23/255), alpha: CGFloat(1))
// path.fill()
color.setStroke()
path.lineWidth = CGFloat(10)
path.stroke()
// setNeedsDisplay()
}
}
override func drawRect(rect: CGRect) {
self.opaque = false;
drawSmallCircles()
}
but still the same black color,
i did what you said and i called the setStock method on the color and i changed the colors values to 0 between 255
If you refer to the documentation for initWithRed:green:blue:alpha:, it points out that those should be values between 0.0 and 1.0.
Furthermore, you never call setStroke to set the line color. You call color.setStroke() to set the color, and then call path.stroke() to draw the stroke at the previously specified color.
--
You might want something like:
func drawSmallCircles(){
for oneArray in points {
let startAngleRadiant: CGFloat = degreesToRadians(Double(oneArray[0]))
let endAngleRadiant: CGFloat = degreesToRadians(Double(oneArray[1]))
let radius: CGFloat = 100.0
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngleRadiant, endAngle: endAngleRadiant, clockwise: true)
let color = UIColor(red: 55.0/255.0, green: 37.0/255.0, blue: 23.0/255.0, alpha: 1.0)
color.setStroke()
path.lineWidth = CGFloat(10)
path.stroke()
}
}
Note, that's a pretty dark color, so it might look black. You might want to lighten that up (using a few values closer to 1.0) to see the color more clearly.

Resources