1
votes

I have to create this shape like the below image in a UIView. But I am not get any idea how to draw this shape. I am trying to draw this shape using UIBezierPath path with CAShapeLayer the circular line draw successfully but I am unable to draw the circular points and circular fill colour. Can anyone please suggest me how I can achieve this type shape using any library or UIBezierPath.

Design Image

This is my code which I am using try to draw this circular shape.

class ViewController: UIViewController {


var firstButton = UIButton()
var mylabel = UILabel()

override func viewDidLoad() {
    super.viewDidLoad()

    self.creatingLayerWithInformation()

}

func creatingLayerWithInformation(){
    let safeAreaHeight = self.view.safeAreaInsets.top
    let navBarHeight = self.navigationController?.navigationBar.frame.height

    self.addLayer(isClockWise: true, radius: self.view.frame.width * 0.72, xPoint: 0, yPoint: navBarHeight!, layerColor: UIColor.green, fillcolor: .clear)
    self.addLayer(isClockWise: true, radius: self.view.frame.width * 0.72, xPoint: self.view.frame.width, yPoint:  self.view.frame.height - 150, layerColor: UIColor.blue, fillcolor: .clear)

    let aa = self.view.frame.width * 0.72

    self.addLayer(isClockWise: true, radius: 10, xPoint: 0+aa, yPoint: navBarHeight!+5, layerColor: UIColor.blue, fillcolor: .clear)

    self.addLayer(isClockWise: true, radius: 10, xPoint: 0+15, yPoint: navBarHeight!+aa, layerColor: UIColor.blue, fillcolor: .clear)

}

func addLayer(isClockWise: Bool, radius: CGFloat, xPoint: CGFloat, yPoint: CGFloat, layerColor: UIColor, fillcolor: UIColor) {

    let pi = CGFloat(Float.pi)
    let start:CGFloat = 0.0
    let end :CGFloat = 20

    // circlecurve
    let path: UIBezierPath = UIBezierPath();
    path.addArc(
        withCenter: CGPoint(x:xPoint, y:yPoint),
        radius: (radius),
        startAngle: start,
        endAngle: end,
        clockwise: isClockWise
    )
    let layer = CAShapeLayer()
    layer.lineWidth = 3
    layer.fillColor = fillcolor.cgColor
    layer.strokeColor = layerColor.cgColor
    layer.path = path.cgPath
    self.view.layer.addSublayer(layer)
}}

But I am getting the below result.

enter image description here

Please suggest me how I can achieve this shape. Correct me if I am doing anything wrong. If there is any library present then also please suggest. Please give me some solution.

Advance thanks to everyone.

2
Have you tried making your start and end angles of the larger circles the same as the locations of the smaller circles?Sam King

2 Answers

4
votes

There are many ways to achieve this effect, but a simple solution is to not draw the large circle as a single arc, but rather as a series of arcs that start and stop at the edges of the smaller circles. To do this, you need to know what the offset is from the inner circles. Doing a little trigonometry, you can calculate that as:

let angleOffset = asin(innerRadius / 2 / mainRadius) * 2

Thus:

let path = UIBezierPath()
path.move(to: point(from: arcCenter, radius: mainRadius, angle: startAngle))

let anglePerChoice = (endAngle - startAngle) / CGFloat(choices.count)
let angleOffset = asin(innerRadius / 2 / mainRadius) * 2
var from = startAngle
for index in 0 ..< choices.count {
    var to = from + anglePerChoice / 2 - angleOffset
    path.addArc(withCenter: arcCenter, radius: mainRadius, startAngle: from, endAngle: to, clockwise: true)
    to = from + anglePerChoice
    from += anglePerChoice / 2 + angleOffset
    path.move(to: point(from: arcCenter, radius: mainRadius, angle: from))
    path.addArc(withCenter: arcCenter, radius: mainRadius, startAngle: from, endAngle: to, clockwise: true)

    from = to
}

let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = lineWidth
layer.addSublayer(shapeLayer)

Where:

func point(from point: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
    return CGPoint(x: point.x + radius * cos(angle),
                   y: point.y + radius * sin(angle))
}

That yields:

enter image description here

So, when you then add the inner circles:

enter image description here


While the above is simple, it has limitations. Specifically if the lineWidth of the big arc was really wide in comparison to that of the small circles, the breaks in the separate large arcs won’t line up nicely with the edges of the small circles. E.g. imagine that the small circles had a radius 22 points, but that the big arc’s stroke was comparatively wide, e.g. 36 points.

enter image description here

If you have this scenario (not in your case, but for the sake of future readers), the other approach, as matt suggested, is to draw the big arc as a single stroke, but then mask it using the paths for the small circles.

So, imagine that you had:

  • a single CAShapeLayer, called mainArcLayer, for the big arc; and

  • an array of UIBezierPath, called smallCirclePaths, for all of the small circles.

You could then create a mask in layoutSubviews of your UIView subclass (or viewDidLayoutSubviews in your UIViewController subclass) like so:

override func layoutSubviews() {
    super.layoutSubviews()

    let path = UIBezierPath(rect: bounds)
    smallCirclePaths.forEach { path.append($0) }

    let mask = CAShapeLayer()
    mask.fillColor = UIColor.white.cgColor
    mask.strokeColor = UIColor.clear.cgColor
    mask.lineWidth = 0
    mask.path = path.cgPath
    mask.fillRule = .evenOdd

    mainArcLayer.mask = mask
}

That yields:

enter image description here

This is a slightly more generalized solution to this problem.

1
votes

Think about the problem in the simplest possible form. Imagine a straight line with one small circle superimposed over the middle of it.

Let's divide our thinking into three layers:

  • The background that needs to show through

  • The layer that holds the straight line

  • The layer that holds the small circle

Calculate the location of the small circle layer relative to the straight line layer.

Place the small circle layer at that location. Okay, but the straight line shows through.

Go back to the straight line layer. Give it a mask. Construct that mask with a transparent circle at exactly the location of the small circle layer.

Now the mask "punches a hole" through the straight line — at exactly the place where the circle covers it. Thus we appear to see through the circle to the background, because the straight line is missing at exactly that place.

In real life there will be multiple circle layers and the mask will have multiple transparent circles and the straight line will be a curve, but that is a minor issue once you have the hole-punching worked out.