Drawing rounders corners on 315° arc - swift

I am drawing an arc that is 270° and has rounded coners on both ends. This is working fine, however now I would like to change my arch to be 315° (-45°), but then my corner calculation won't work.
I have tried to calculate this different ways but can't seem to find the formula for making a general function to add rounded corners to my arc when the start and ends are not vertical och horizontal.
This is my playground code:
import UIKit
import PlaygroundSupport
class ArcView: UIView {
private var strokeWidth: CGFloat {
return CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
}
private let cornerRadius: CGFloat = 10
override open func draw(_ rect: CGRect) {
super.draw(rect)
backgroundColor = UIColor.white
drawNormalCircle()
}
func drawNormalCircle() {
let strokeWidth = CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
let innerRadius = (min(self.bounds.width, self.bounds.height) - strokeWidth*2) / 2.0
let outerRadius = (min(self.bounds.width, self.bounds.height)) / 2.0
var endAngle: CGFloat = 270.0
let bezierPath = UIBezierPath(arcCenter: self.center, radius: outerRadius, startAngle: 0, endAngle: endAngle * .pi / 180, clockwise: true)
var point = bezierPath.currentPoint
point.y += cornerRadius
let arc = UIBezierPath(arcCenter: point, radius: cornerRadius, startAngle: 180 * .pi / 180, endAngle: 270 * .pi / 180, clockwise: true)
arc.apply(CGAffineTransform(rotationAngle: (360 - endAngle) * .pi / 180))
var firstCenter = bezierPath.currentPoint
firstCenter.y += cornerRadius
bezierPath.addArc(withCenter: firstCenter, radius: cornerRadius , startAngle: 270 * .pi / 180 , endAngle: 0, clockwise: true)
bezierPath.addLine(to: CGPoint(x: bezierPath.currentPoint.x, y: strokeWidth - cornerRadius))
var secondCenter = bezierPath.currentPoint
secondCenter.x -= cornerRadius
bezierPath.addArc(withCenter: secondCenter, radius: cornerRadius , startAngle: 0, endAngle: 90 * .pi / 180, clockwise: true)
bezierPath.addArc(withCenter: self.center, radius: innerRadius, startAngle: 270 * .pi / 180, endAngle: 0, clockwise: false)
var thirdCenter = bezierPath.currentPoint
thirdCenter.x += cornerRadius
bezierPath.addArc(withCenter: thirdCenter, radius: cornerRadius , startAngle: 180 * .pi / 180, endAngle: 270 * .pi / 180, clockwise: true)
bezierPath.addLine(to: CGPoint(x: bezierPath.currentPoint.x + strokeWidth - (cornerRadius * 2), y: bezierPath.currentPoint.y))
var fourthCenter = bezierPath.currentPoint
fourthCenter.y += cornerRadius
bezierPath.addArc(withCenter: fourthCenter, radius: cornerRadius , startAngle: 270 * .pi / 180, endAngle: 0, clockwise: true)
bezierPath.close()
let backgroundLayer = CAShapeLayer()
backgroundLayer.path = bezierPath.cgPath
backgroundLayer.strokeColor = UIColor.red.cgColor
backgroundLayer.lineWidth = 2
backgroundLayer.fillColor = UIColor.lightGray.cgColor
self.layer.addSublayer(backgroundLayer)
}
}
let arcView = ArcView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
PlaygroundPage.current.liveView = arcView
The problem for me is how to calculate the arc center for the corners when the corner is not a given X - CornerRadius, or Y + corner Radius, which it is in perfectly horizontal or vertical cases. How can I have rounded corners when the arc is 315°.

Preface: usually when my answer to a question is "do something completely different," I aim to fix the original problem as is, and then also suggest the better approach in addition. However, that's unfeasible for this one, because the complexity of this code, if it were to be expanded in the same style, grows so immensely that it just wasn't worth it.
The problem here is fundamentally that of code organization. Many expressions are repeated by copy-pasting. Extracting them to a variable would not only give a central place for editing, but it also gives a name to the expression, immensely improving readability.
This code will be long. But that's okay, because it's going to be simple. Having a bunch of simple stuff almost always beats a small amount of complex stuff. You might be able to write some crazy trig code that lays out your bezier curve perfectly, but chances are, you won't get it right the first time. Debugging will be hard. It will be totally foreign, and way harder to anyone who isn't you ...and that includes future you. Future you will struggle.
Before we start, here's a rough diagram to help you orient yourself with the rest of this post:
A visual development environment
Firstly, we need to establish a good way to visualize our results.
Playground can quickly reload previews, so that's a plus. But debugging with closed paths is hard, because it's hard to tell the individual sections of a bezier path apart, and often times for convex shapes, the path will close over itself in a way that obscures the part of the path you're working on. So the first part of the code I would work on, is an abstraction layer for UIBezierPath
To remedy this, I'm going to develop the shape by stroking each section in a different colour. Unfortunately, you can't stroke a subsection of a UIBezierPath separately from the rest, so to achieve this, our shape will need to be composed of multiple UIBezierPaths, stroking each one as we go along. But this could be slow in performance-sensitive contexts, so ideally we only want to be doing this during development. I want to be able to pick between one of two different ways to do the same thing. Protocols are perfect for that, so let's start there.
I'll start with BezierPathBuilder. All it does is allow me append BezierPathRenderable parts to it (which I'll get to later), and build a final path, which I can hand off to my CALayer or whatever.
protocol BezierPathBuilder: AnyObject {
func append(_: BezierPathRenderable)
func build() -> UIBezierPath
}
The main implementation of this protocol is really simple, it just wraps a UIBezierPath. When the renderable is told to render itself, it'll simply operate on the path we give it, without needing to allocate any intermediate paths.
class BezierPathBuilderImpl: BezierPathBuilder {
let path = UIBezierPath()
func append(_ renderable: BezierPathRenderable) {
renderable.render(into: self, self.path)
}
func build() -> UIBezierPath {
return path
}
}
The debug implementation is a bit more interesting. When appending a renderable, we don't let it draw itself directly into our main path. Instead, we make a new temporary path for it to use, where it'll draw itself. We then have the opportunity to stroke that path (with a different colour every time). Once we've done that, we can append that temporary path to the main path, and resume.
class DebugBezierPathBuilder: BezierPathBuilder {
var rainbowIterator = ([
.red, .orange, .yellow, .green, .cyan, .blue, .magenta, .purple ] as Array<UIColor>).makeIterator()
let path = UIBezierPath()
func append(_ renderable: BezierPathRenderable) {
let newPathSegment = UIBezierPath()
renderable.render(into: self, newPathSegment)
// This will crash if you use too many colours, but it suffices for now.
rainbowIterator.next()!.setStroke()
newPathSegment.lineWidth = 20
newPathSegment.stroke()
path.append(newPathSegment)
}
func build() -> UIBezierPath {
return path
}
}
Objectifying our geometry
In your code, there's no separation between geometry calculations and drawing. As a result, you can't easily define one component in reference to another, because you don't have a way of "fishing out" that last arc you drew in the UIBezierPath, or whatever. So let's remedy that.
First of all, I'll define a protocol, BezierPathRenderable, which our program will use to define what it means for an entity to be renderable to a BezierPath.
protocol BezierPathRenderable {
func render(into builder: BezierPathBuilder, _ path: UIBezierPath)
}
This design isn't my favourite, but it's the best I could come up with. The two parameters here allow the conforming type to either draw itself directly into the path, or call append on the builder. The latter is useful for aggregate shapes that are composed out of simpler constituents (sound familiar?)
Wish oriented development
My favourite process for writing code involves scaffolding out a whole bunch of stuff early on, writing whatever parts of the code are interesting to me, and just adding stub implementations left-and-right. Every step of the way, I'm essentially answering the question "what API do I wish I had right now?", and then I stub it, and pretend that it exists.
Stubbing the main shape
I'll start with the main structure. We want an object that models a sector of an annulus with rounded corners. This object would have to do two things:
Store all values that parameterized shape
Define a way to render the shape, by conforming to BezierPathRenderable
So let's start with that:
struct RoundedAnnulusSector: BezierPathRenderable {
let center: CGPoint
var innerRadius: CGFloat
var outerRadius: CGFloat
var startAngle: Angle
var endAngle: Angle
var cornerRadius: CGFloat
func render(into builder: BezierPathBuilder, _ path: BezierPath) {
/// ???
}
Preparing some rendering code
Let's write some scaffolding code to draw this using our new rendering system. For now I'll be using our debugging strokes to render, so I'll comment out the CAShapeLayer stuff:
import UIKit
import PlaygroundSupport
class ArcView: UIView {
private var strokeWidth: CGFloat {
return CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
}
override open func draw(_ rect: CGRect) {
super.draw(rect)
self.backgroundColor = UIColor.white
let innerRadius = (min(self.bounds.width, self.bounds.height) - strokeWidth*2) / 2.0
let outerRadius = (min(self.bounds.width, self.bounds.height)) / 2.0
let shape = RoundedAnnulusSector(
center: self.center,
innerRadius: innerRadius - 50,
outerRadius: outerRadius - 50,
startAngle: (45 * .pi) / 180,
endAngle: (315 * .pi) / 180,
cornerRadius: 25
)
let builder = DebugBezierPathBuilder()
builder.append(shape)
let path = builder.build()
let backgroundLayer = CAShapeLayer()
// backgroundLayer.path = path.cgPath
// backgroundLayer.strokeColor = UIColor.red.cgColor
// backgroundLayer.lineWidth = 2
// backgroundLayer.fillColor = UIColor.lightGray.cgColor
self.layer.addSublayer(backgroundLayer)
}
}
let arcView = ArcView(frame: CGRect(x: 0, y: 0, width: 800, height: 800))
PlaygroundPage.current.liveView = arcView
This obviously does nothing, because we haven't implemented RoundedAnnulusSector.render(into:_:).
Stubbing the corners
We can notice that the entirety of this shape's drawing hinges upon the details of its 4 corners. If our shape has four corners, why don't we just say that?
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var corner1: RoundedAnnulusSectorCorner { ??? }
private var corner2: RoundedAnnulusSectorCorner { ??? }
private var corner3: RoundedAnnulusSectorCorner { ??? }
private var corner4: RoundedAnnulusSectorCorner { ??? }
}
In writing that, I wished that a structure existed called RoundedAnnulusSectorCorner, which would do two things:
Store all values that parameterized shape
Define a way to render the shape, by conforming to BezierPathRenderable
Notice they're the same two roles that RoundedAnnulusSector fulfills. These things are intentionally simple, and meant to be composable.
For now we can just stub RoundedAnnulusSectorCorner
struct RoundedAnnulusSectorCorner {}
...and fill in our computed properties to return default instances. Next we'll want to define the inner and outer arcs of our shape.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var corner1: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
private var corner2: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
private var corner3: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
private var corner4: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
private var outerArc: Arc { ??? }
private var innerArc: Arc { ??? }
}
Implementing Arc
Again, Arc is just another shape, that'll fulfill the same two roles as the others. From our familiarity with UIBezierPath's arc APIs, we'll know that the arcs would need a center, radius, start/end angle, and an indicator of whether to draw them clockwise or counter-clockwise. So we can fill that out:
struct Arc: BezierPathRenderable {
let center: CGPoint
let radius: CGFloat
let startAngle: CGFloat
let endAngle: CGFloat
let clockwise: Bool
func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
path.addArc(withCenter: center, radius: radius,
startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
}
}
First approximation of innerArc/outerArc
Now we need to determine the arguments to initialize our arcs with. We'll start without rounded corners, so we'll just use our startAngle/endAngle directly, with our innerRadius/outerRadius.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var outerArc: Arc {
return Arc(
center: self.center,
radius: self.outerRadius,
startAngle: self.startAngle,
endAngle: self.endAngle,
clockwise: true
)
}
private var innerArc: Arc {
return Arc(
center: self.center,
radius: self.innerRadius,
startAngle: self.endAngle,
endAngle: self.startAngle,
clockwise: false
)
}
}
Initial rendering
With these two parts complete, we can start drawing to see how it looks so far, by doing an initial implementation of RoundedAnnulusSector.render(into:_:)
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
func render(into builder: BezierPathBuilder, _ path: BezierPath) {
let components: [BezierPathRenderable] = [
self.outerArc,
self.innerArc,
]
builder.append(contentsOf: components)
}
}
extension BezierPathBuilder {
func append<S: Sequence>(contentsOf renderables: S) where S.Element == BezierPathRenderable {
for renderable in renderables {
self.append(renderable)
}
}
}
As we progress, we can add more BezierPathRenderable components to this list. I saw this coming, so I made that BezierPathBuilder for handling sequences, so we can just feed it an array and have it automatically append all the elements within.
Stubbing startAngleEdge, endAngleEdge
This shape needs two straight lines. The first will connect corner 4 with corner 1 (which will be a radial line outwards from the center along the startAngle), and the second will connect corner 2 with corner 3 (which will be a radial line outwards from the center along the endAngle. Let's put those in:
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
func render(into builder: BezierPathBuilder, _ path: BezierPath) {
let components: [BezierPathRenderable] = [
self.outerArc,
self.endAngleEdge,
self.innerArc,
self.startAngleEdge,
]
builder.append(contentsOf: components)
}
// ...
private var endAngleEdge: Line {
return Line()
}
private var startAngleEdge: Line {
return Line()
}
}
Implementing Line
We can just stub out Line, but we know a line just connects two points. It's so simple we may as well just finish it:
struct Line: BezierPathRenderable {
let start: CGPoint
let end: CGPoint
func render(into builder: BezierPathBuilder, _ path: BezierPath) {
path.move(to: self.start)
path.addLine(to: self.end)
}
}
Implementation of startAngleEdge/endAngleEdge
Now we need to figure out what the start/end points will be for our two lines. It would be really convenient if our RoundedAnnulusSectorCorner had startPoint: CGPoint and endPoint: CGPoint properties.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var endAngleEdge: Line {
return Line(
start: self.corner2.endPoint,
end: self.corner3.startPoint)
}
private var startAngleEdge: Line {
return Line(
start: self.corner4.endPoint,
end: self.corner1.startPoint)
}
}
First approximation of startAngleEdge/endAngleEdge
Let's fulfill our wish
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
var startPoint: CGPoint { return .zero }
var endPoint: CGPoint { return .zero }
}
Because these are all implemented as CGPoint.zero, none of our edges would draw.
Second approximation of startAngleEdge/endAngleEdge
So let's implement some better approximation of startPoint/endPoint. Suppose our point had a rawCornerPoint: CGPoint. This would be a point that would be the location of the corner, if there was no rounding (i.e. rounding radius = 0). In the no-rounding world, our startPoint/endPoint would both be rawCornerPoint. Let's stub it and use it:
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
var rawCornerPoint: CGPoint { return .zero }
var startPoint: CGPoint { return self.rawCornerPoint }
var endPoint: CGPoint { return self.rawCornerPoint }
}
Implementing rawCornerPoint
Now, we'll need to derive it's real value. rawCornerPoint depends on two things:
the center of the parent shape,
the position of the corner, relative to the center of the parent shape. This itself relies on:
the distance to the parent shape's center
the angle relative to the parent shape's center
Each of these things is a parameter of our parent shape, so these properties will actually be stored (and initialized by the parent shape). We can then use them to compute the offset, and add that offset to the parentCenter.
struct RoundedAnnulusSectorCorner {
let parentCenter: CGPoint
let distanceToParentCenter: CGFloat
let angleToParentCenter: CGFloat
var rawCornerPoint: CGPoint {
let inset = CGPoint(
radius: self.distanceToParentCenter,
angle: self.angleToParentCenter
)
return self.parentCenter + inset
}
// ...
}
Clearly, an initializer for CGPoint that initializes it from polar coordinates would be just wonderful.
Also, writing .applying(CGAffineTransform(translationX: deltaX, y: deltaY) is annoying, it would be nice to just have a + operator.
Implementing some CGPoint utilities
Let's fulfill more of our wishes:
// Follows UIBezierPath convention on angles.
// 0 is "right" at 3 o'clock, and angle increase clockwise.
extension CGPoint {
init(radius: CGFloat, angle: CGFloat) {
self.init(x: radius * cos(angle), y: radius * sin(angle))
}
static func + (l: CGPoint, r: CGPoint) -> CGPoint {
return CGPoint(x: l.x + r.x, y: l.y + r.y)
}
static func - (l: CGPoint, r: CGPoint) -> CGPoint {
return CGPoint(x: l.x - r.x, y: l.y - r.y)
}
}
Populating the corners' fields
Now that our corners actually have stored properties, we can go back to our RoundedAnnulusSector and fill them in.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var corner1: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
distanceToParentCenter: self.outerRadius,
angleToParentCenter: self.startAngle
)
}
private var corner2: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
distanceToParentCenter: self.outerRadius,
angleToParentCenter: self.endAngle
)
}
private var corner3: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
distanceToParentCenter: self.innerRadius,
angleToParentCenter: self.endAngle
)
}
private var corner4: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
distanceToParentCenter: self.innerRadius,
angleToParentCenter: self.startAngle
)
}
// ...
}
Second rendering
Our main shape's render list already contains our lines, but now that we've implemented an approximation of them, we can actually test them. If I've explained things correctly so far, at this point there should be a closed-looking shape, with sharp corners. Success! (I hope)
Rounding things off
Makes it sound like we're almost done, but no way haha, this is where the good stuff starts.
Firstly, we should add all the stuff that comes with roundedness. We know our rounded corners are going to be arcs, and luckily, we've already implemented those!
Arcs need a start and end angle, so we'll need those too. We can stub them with 0 and 2π, so that we don't have to worry about the orientation of our rounded corners' arcs. For now, they'll just be full circles.
struct RoundedAnnulusSectorCorner {
// ...
let radius: CGFloat
var arc: Arc {
return Arc(center: rawCornerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
}
/// The angle at which this corner's arc starts.
var startAngle: CGFloat { return 0 }
/// The angle at which this corner's arc ends.
var endAngle: CGFloat { return 2 * .pi }
}
Third rendering
Now that our corners have arcs, we can add those arcs to our render list for drawing:
struct RoundedAnnulusSector: BezierPathRenderable {
let center: CGPoint
let innerRadius: CGFloat
let outerRadius: CGFloat
let startAngle: CGFloat
let endAngle: CGFloat
let cornerRadius: CGFloat
func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
let components: [BezierPathRenderable] = [
self.corner1.arc,
self.outerArc,
self.corner2.arc,
self.endAngleEdge,
self.corner3.arc,
self.innerArc,
self.corner4.arc,
self.startAngleEdge,
]
builder.append(contentsOf: components)
}
}
And behold, circles!
But uh'oh, they're centred around the rawCornerPoint. I suppose that shouldn't be surprising, because that's literally how we defined our Arc.
Insetting the center
But to get this right, we need to inset the center of the arc. Let's call it the center. The center needs to be inset, so that it's closer towards the "inside" of RoundedAnnulusSector, so that after adding the corner radius, the stroke lines up with the rest of the shape.
This insetting consists of two components:
1. The center needs to be rotated, relative to the parentCenter by an angle (call it rotationalInsetAngle), which makes it lay on the inner or outer arc, so that its arc extends rotationally to reach the radius
Here's a photo for reference:
The blue circle is our corner's current circle, centred on rawCornerPoint.
The small arrow marked 1 is the rotational component of the inset.
The pink circle is our corner's circle, after the rotational inset.
The small arrow marked 2 is the radial translation component of the inset
The green circle is our desired corner circle, centred on center.
The dashed arrow marked offset is the offset between rawCornerPoint and center, obtained as a composition of the rotional and radial translation components.
struct RoundedAnnulusSectorCorner {
// ...
/// The center of this rounded corner's arc
///
/// ...after insetting from the `rawCornerPoint`, so that this rounded corner's arc
/// aligns perfectly with the curves adjacent to it.
var center: CGPoint {
return self.rawCornerPoint
.rotated(around: self.parentCenter, by: self.rotationalInsetAngle)
.translated(towards: self.edgeAngle, by: self.radialInsetDistance)
}
}
We have a long wish list here: rotationalInsetAngle, radialInsetDistance, CGPoint.rotated(around:by:), CGPoint.translated(towards:, by:).
More CGPoint utilities
Luckily, these are pretty easy to implement, now that we have the polar initializer.
extension CGPoint {
func translated(towards angle: CGFloat, by r: CGFloat) -> CGPoint {
return self + CGPoint(radius: r, angle: angle)
}
func rotated(around pivot: CGPoint, by angle: CGFloat) -> CGPoint {
return (self - pivot).applying(CGAffineTransform(rotationAngle: angle)) + pivot
}
}
Getting stuck with rotationalInsetAngle, radialInsetDistance
This is where the shit hits the fan. We know that we have to inset our corner by translating it over by radius. But in which direction?
The inner two corners (#3 and #4) need to translate radially away from the parent, whereas the outer two corners (#1 and #3) need to translate radially inwards towards the parent.
Similarly, our rotional inset also need to vary. For the two corners on the starting edge (#1 and #4), we need to inset clockwise from the startEdge, whereas the two corners on the ending edge (#2 and #3) need to inset by counter-clockwise from the endEdge.
But our data model so far only tells our corners where they are in terms of an angle and a distance. It doesn't specify which side they need to stay on relative the defined distanceFromCenter and angleToParentCenter.
This will need some pretty big refactoring.
Implementing RadialPosition and RotationalPosition
Lets implement a type called RadialPosition. It'll capture not just a radial position (i.e. a distance form a central point), but also which side of that distance to "stay on". A struct containing a radialDistance: CGFloat and isInsideOfRadialDistance: Bool would do, but I know I've frequently made lots of bugs stemming from improperly handling conditions. Instead, I'll use a two case enum, where the inside/outside distinction is more explicit, and harder to miss. Because enum associated values are cumbersome to access, I'll add a helper computed property, distanceFromCenter, to hide away that annoying switch statement.
struct RoundedAnnulusSectorCorner {
// ...
enum RadialPosition {
case outside(ofRadius: CGFloat)
case inside(ofRadius: CGFloat)
var distanceFromCenter: CGFloat {
switch self {
case .outside(ofRadius: let d), .inside(ofRadius: let d): return d
}
}
}
// ...
}
Next, I'll do a similar thing for RotationalPosition:
struct RoundedAnnulusSectorCorner {
// ...
enum RotationalPosition {
case cw(of: CGFloat)
case ccw(of: CGFloat)
var edgeAngle: CGFloat {
switch self {
case .cw(of: let angle), .ccw(of: let angle): return angle
}
}
}
// ...
}
Now I'll have to remove the existing distanceToParentCenter: CGFloat and angleToParentCenter: CGFloat properties and replace them with these new models. We need to migrate their call sites to radialPosition.distanceFromCenter and RotationalPosition. edgeAngle. This is the final set of stored properties for RoundedAnnulusSectorCorner:
struct RoundedAnnulusSectorCorner {
let parentCenter: CGPoint
let radius: CGFloat
let radialPosition: RadialPosition
let rotationalPosition: RotationalPosition
// ...
/// The location of the corner, if this rounded wasn't rounded.
private var rawCornerPoint: CGPoint {
let inset = CGPoint(
radius: self.radialPosition.distanceFromCenter,
angle: self.rotationalPosition.edgeAngle
)
return self.parentCenter + inset
}
// ...
}
And we'll have to update our corner definitions to provide this new data. These are the final set of definitions for the corners.
struct RoundedAnnulusSector: BezierPathRenderable {
// ...
private var corner1: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .inside(ofRadius: self.outerRadius),
rotationalPosition: .cw(of: self.startAngle)
)
}
private var corner2: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .inside(ofRadius: self.outerRadius),
rotationalPosition: .ccw(of: self.endAngle)
)
}
private var corner3: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .outside(ofRadius: self.innerRadius),
rotationalPosition: .ccw(of: self.endAngle)
)
}
private var corner4: RoundedAnnulusSectorCorner {
return RoundedAnnulusSectorCorner(
parentCenter: self.center,
radius: self.cornerRadius,
radialPosition: .outside(ofRadius: self.innerRadius),
rotationalPosition: .cw(of: self.startAngle)
)
}
// ...
}
Fourth rendering
Running this code again, we see that now the circles are still centred on their rawCornerPoint, but this is good. It means our refactoring hasn't broken our already-working features. If we had unit tests all along, this is where they'd be useful.
To be continued
in my next answer, because I just got a StackOverflow error that I double many people have had before:

That's my solution.
It's just simple trigonometry
/// Create a path made with 6 small subpaths
///
/// - Parameters:
/// - startAngle: the start angle of the path in cartesian plane angles system
/// - endAngle: the end angle of the path in cartesian plane angles system
/// - outerRadius: the radius of the outer circle in % relative to the size of the view that holds it
/// - innerRadius: the radius of the inner circle in % relative to the size of the view that holds it
/// - cornerRadius: the corner radius of the edges
///
/// - Returns: the path itself
func createPath(from startAngle: Double, to endAngle: Double,
outerRadius:CGFloat, innerRadius:CGFloat,
cornerRadius: CGFloat) -> UIBezierPath {
let path = UIBezierPath()
let maxDim = min(view.frame.width, view.frame.height)
let oRadius: CGFloat = maxDim/2 * outerRadius
let iRadius: CGFloat = maxDim/2 * innerRadius
let center = CGPoint.init(x: view.frame.width/2, y: view.frame.height/2)
let startAngle = deg2rad(360.0 - startAngle)
let endAngle = deg2rad(360.0 - endAngle)
// Outer Finish Center point
let ofcX = center.x + (oRadius - cornerRadius) * CGFloat(cos(endAngle - deg2rad(360)))
let ofcY = center.y + (oRadius - cornerRadius) * CGFloat(sin(endAngle - deg2rad(360)))
// Inner Finish Center point
let ifcX = center.x + (iRadius + cornerRadius) * CGFloat(cos(endAngle - deg2rad(360)))
let ifcY = center.y + (iRadius + cornerRadius) * CGFloat(sin(endAngle - deg2rad(360)))
// Inner Starting Center point
let iscX = center.x + (iRadius + cornerRadius) * CGFloat(cos(startAngle - deg2rad(360)))
let iscY = center.y + (iRadius + cornerRadius) * CGFloat(sin(startAngle - deg2rad(360)))
// Outer Starting Center point
let oscX = center.x + (oRadius - cornerRadius) * CGFloat(cos(startAngle - deg2rad(360)))
let oscY = center.y + (oRadius - cornerRadius) * CGFloat(sin(startAngle - deg2rad(360)))
// Outer arch
path.addArc(withCenter: center, radius: oRadius,
startAngle: startAngle, endAngle: endAngle,
clockwise: true)
// Rounded outer finish
path.addArc(withCenter: CGPoint(x: ofcX, y: ofcY), radius: cornerRadius,
startAngle: endAngle, endAngle:endAngle + deg2rad(90),
clockwise: true)
// Rounded inner finish
path.addArc(withCenter: CGPoint(x: ifcX, y: ifcY), radius: cornerRadius,
startAngle: endAngle + deg2rad(90), endAngle: endAngle + deg2rad(180),
clockwise: true)
// Inner arch
path.addArc(withCenter: center, radius: iRadius,
startAngle: endAngle, endAngle: startAngle,
clockwise: false)
// Rounded inner start
path.addArc(withCenter: CGPoint(x: iscX, y: iscY), radius: cornerRadius,
startAngle: startAngle + deg2rad(180), endAngle: startAngle + deg2rad(270),
clockwise: true)
// Rounded outer start
path.addArc(withCenter: CGPoint(x: oscX, y: oscY), radius: cornerRadius,
startAngle: startAngle + deg2rad(270), endAngle: startAngle,
clockwise: true)
return path
}
func deg2rad(_ number: Double) -> CGFloat {
return CGFloat(number * .pi / 180)
}
Usage:
#IBOutlet weak var mainView: UIView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let borderLayer = CAShapeLayer()
borderLayer.path = createPath(from: 30, to: 120, outerRadius: 0.9, innerRadius: 0.3, cornerRadius: 5).cgPath
borderLayer.strokeColor = UIColor.orange.cgColor
borderLayer.fillColor = UIColor.orange.cgColor
borderLayer.lineWidth = 0.0
mainView.layer.addSublayer(borderLayer)
}

Continued from the first part of my answer
Another try at rotationalInsetAngle, radialInsetDistance
Now that are corners are aware of what side they are relative to their distance and angle, we can implement our rotationalInsetAngle, radialInsetDistance.
radialInsetDistance is easy. We just move closer or further from the middle, by self.radius, depending on whether we're on the inside or outside.
struct RoundedAnnulusSectorCorner {
// ...
/// The distance towards/away from the disk's center
/// where this corner's center is going to be
internal var radialInsetDistance: CGFloat {
switch self.radialPosition {
case .inside(_): return -self.radius // negative: towards center
case .outside(_): return +self.radius // positive: away from center
}
}
}
The rotationalInsetAngle is a bit trickier, you'll need to bust out a notepad and do your best to recall high school trig.
struct RoundedAnnulusSectorCorner {
// ...
/// The angular inset (in radians) from the disk's edge
/// where this corner's center is going to be
internal var rotationalInsetAngle: CGFloat {
let angle = ???
switch self.rotationalPosition {
case .ccw(_): return -angle // negative: ccw from the edge
case .cw(_): return +angle // postiive: cw from the edge
}
}
}
We know we need to rotate by some angle, called it angle, whose magnitude is always the same, but whose sign depending on whether our corner is before the start or after before the end edge.
We know that after our translation, our corner circle's stroke will overlap the parent shape's edges/arcs. That distance is the self.radius, and it forms the "opposite" side a the right triangle. The hypotenuse is the radial we're rotating around, with a length of self.radialPosition.distanceFromCenter. Given that we have an opposite (o) and a hypotenuse (h), the correct trig function for the job is sin. angle = sin(o / h). In context:
struct RoundedAnnulusSectorCorner {
// ...
/// The angular inset (in radians) from the disk's edge
/// where this corner's center is going to be
internal var rotationalInsetAngle: CGFloat {
let angle = sin(self.radius / self.radialPosition.distanceFromCenter)
switch self.rotationalPosition {
case .ccw(_): return -angle // negative: ccw from the edge
case .cw(_): return +angle // postiive: cw from the edge
}
}
}
Ditching rawCornerPoint
Hippity hoppity, our center computed property should now world properly, because our newly implemented rotationalInsetAngle and radialInsetDistance.
We can update our corners' arcs to use it:
struct RoundedAnnulusSectorCorner {
// ...
var arc: Arc {
return Arc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
}
// ...
}
Fifth rendering
If all has gone well, you should see the corner's circle are now at the right place (centred on centers, not rawCornerPoints).
Decircularifying
Up until now, we've rendered the rounded corners as full circles. That was useful for getting it going, but now we can fix it. Let's properly implement the startAngle/endAngle computed properties to derive the appropriate start and end angles for each rounded corner.
These are easy. Every rounded corner starts going with the edge angle, perpendicular to it, against it, or perpendicular the other way. The arc then continues for a quarter turn, so we can just obtain the endAngle by adding a quarter turn (2π) to startAngle.
struct RoundedAnnulusSectorCorner {
// ...
/// The angle at which this corner's arc starts.
var startAngle: CGFloat {
switch (radialPosition, rotationalPosition) {
case let ( .inside(_), .cw(of: edgeAngle)): return edgeAngle + (3 * .pi/2)
case let ( .inside(_), .ccw(of: edgeAngle)): return edgeAngle + (0 * .pi/2)
case let (.outside(_), .ccw(of: edgeAngle)): return edgeAngle + (1 * .pi/2)
case let (.outside(_), .cw(of: edgeAngle)): return edgeAngle + (2 * .pi/2)
}
}
/// The angle at which this corner's arc ends.
var endAngle: CGFloat {
return self.startAngle + .pi/2 // A quarter turn clockwise from the start
}
}
Sixth rendering
We're almost there! Now our circles are no more. We have rounded corners, centred on the correctly inset center points, arcing in the right arcs.
The only remaining issue, is that our startAngleEdge, endAngleEdge, innerArc, and outerArc don't terminate where the rounded corners terminate.
Fixing the edges
We can now replace our definition of startPoint/endPoint, which until now been computed as being rawCornerPoint.
Computing these is as easy translating our point, towards the start/end angle, by a distance of radius. Oh look, we already made a tool for that!
struct RoundedAnnulusSectorCorner {
// ...
/// The point at which this corner's arc starts.
var startPoint: CGPoint {
return self.center.translated(towards: startAngle, by: radius)
}
/// The point at which this corner's arc ends.
var endPoint: CGPoint {
return self.center.translated(towards: endAngle, by: radius)
}
// ...
}
Seventh rendering
Now our edges are in the right place!
Fixing the arcs
Fixing the arcs is easy. We know each corner's rotationalInsetAngle, we just need to add it to our start/end angles, so that they start/end later/earlier as necessary:
struct RoundedAnnulusSectorCorner {
// ...
private var outerArc: Arc {
return Arc(
center: self.center,
radius: self.outerRadius,
startAngle: self.startAngle + self.corner1.rotationalInsetAngle,
endAngle: self.endAngle + self.corner2.rotationalInsetAngle,
clockwise: true
)
}
private var innerArc: Arc {
return Arc(
center: self.center,
radius: self.innerRadius,
startAngle: self.endAngle + self.corner3.rotationalInsetAngle,
endAngle: self.startAngle + self.corner4.rotationalInsetAngle,
clockwise: false
)
}
// ...
}
Eighth rendering
It's done! Here's the final final gist, though I do strongly encourage following along and learning about the process.

Related

Why does path draw towards the opposite direction?

I have the following code snippet that draws a circular sector shape:
struct CircularSector: Shape {
let centralAngle: Angle
func path(in rect: CGRect) -> Path {
let radius = min(rect.width, rect.height) / 2
let center = CGPoint(x: rect.midX, y: rect.midY)
var path = Path()
path.addArc(center: center, radius: radius, startAngle: .degrees(0), endAngle: centralAngle, clockwise: true)
path.addLine(to: center)
path.closeSubpath()
return path
}
}
When I preview it,
struct CircularSector_Previews: PreviewProvider {
static var previews: some View {
CircularSector(centralAngle: .degrees(45)).fill(Color.black)
}
}
}
instead of a 45° sector clockwise, it draws a 315° sector counterclockwise. Is this the correct behaviour, or did I do something wrong?
It does seem like a bug. (See https://stackoverflow.com/a/57034585/341994 where exactly the same thing happens.) This is not how UIBezierPath behaves over on the UIKit side. If we say, mutatis mutandis:
let path = UIBezierPath()
path.addArc(withCenter: center, radius: radius,
startAngle: 0, endAngle: .pi/4, clockwise: true)
path.addLine(to: center)
path.close()
We get
which is just what you are expecting. It's easy to see how to compensate, but it does seem that what you are compensating for is a mistake in the SwiftUI Path implementation.

Specifying UIBezierPath points with corner radii like in drawing apps (e.g. Sketch)

In drawing apps such as Sketch, when you draw a vector, you can specify a custom corner radius for each point individually.
For example, here is a 5 point vector, with the middle point's corner radius set to 17:
(The top left and bottom right points have custom radii as well.)
In Swift, I can draw paths using a UIBezierPath, but when I specify addLine, I'm only given an option to specify a point. I don't have the option to specify a corner radius.
How do I give addLine a corner radius?
It seems like I should be able to use either addCurve or addArc to achieve what I want, but I'm not sure what values to supply to those to get the desired results.
Instead of addLine, you must use addArc. The values for addArc are dependent on the previous/next points. Thus any helper function should take in the entire array of points you want to use.
If you can figure out how to draw a curve between two of the lines, the same algorithm could be repeated for the entire shape.
To make things easier to understand, refer to this diagram (the code will reference these points as well):
The goal is to find out the circle's center and the start/end angles.
First, you should grab the CGFloat and CGPoint extensions from here.
Next add these helper functions:
extension Collection where Index == Int {
func items(at index: Index) -> (previous: Element, current: Element, next: Element) {
precondition(count > 2)
let previous = self[index == 0 ? count - 1 : index - 1]
let current = self[index]
let next = self[(index + 1) % count]
return (previous, current, next)
}
}
/// Returns ∠abc (i.e. clockwise degrees from ba to bc)
//
// b - - - a
// \
// \
// \
// c
//
func angleBetween3Points(_ a: CGPoint, _ b: CGPoint, _ c: CGPoint) -> CGFloat {
let xbaAngle = (a - b).angle
let xbcAngle = (c - b).angle // if you were to put point b at the origin, `xbc` refers to the angle formed from the x-axis to the bc line (clockwise)
let abcAngle = xbcAngle - xbaAngle
return CGPoint(angle: abcAngle).angle // normalize angle between -π to π
}
func arcInfo(
previous: CGPoint,
current: CGPoint,
next: CGPoint,
radius: CGFloat)
-> (center: CGPoint, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)
{
let a = previous
let b = current
let bCornerRadius: CGFloat = radius
let c = next
let abcAngle: CGFloat = angleBetween3Points(a, b, c)
let xbaAngle = (a - b).angle
let abeAngle = abcAngle / 2
let deLength: CGFloat = bCornerRadius
let bdLength = bCornerRadius / tan(abeAngle)
let beLength = sqrt(deLength*deLength + bdLength*bdLength)
let beVector: CGPoint = CGPoint(angle: abcAngle/2 + xbaAngle)
let e: CGPoint = b + beVector * beLength
let xebAngle = (b - e).angle
let bedAngle = (π/2 - abs(abeAngle)) * abeAngle.sign() * -1
return (
center: e,
startAngle: xebAngle - bedAngle,
endAngle: xebAngle + bedAngle,
clockwise: abeAngle < 0)
}
func addArcs(to path: UIBezierPath, pointsAndRadii: [(point: CGPoint, radius: CGFloat)]) {
precondition(pointsAndRadii.count > 2)
for i in 0..<pointsAndRadii.count {
let (previous, current, next) = pointsAndRadii.items(at: i)
let (center, startAngle, endAngle, clockwise) = arcInfo(
previous: previous.point,
current: current.point,
next: next.point,
radius: current.radius)
path.addArc(withCenter: center, radius: current.radius, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
}
}
Finally, you can now use these functions to render any vector defined in a drawing app easily:
override func draw(_ rect: CGRect) {
let grayPath = UIBezierPath()
addArcs(
to: grayPath,
pointsAndRadii: [
(point: CGPoint(x: 100, y: 203), radius: 0),
(point: CGPoint(x: 100, y: 138.62), radius: 33),
(point: CGPoint(x: 173.78, y: 100), radius: 0),
(point: CGPoint(x: 139.14, y: 172.51), radius: 17),
(point: CGPoint(x: 231, y: 203), radius: 3),
])
grayPath.close()
grayPath.lineWidth = 5
UIColor.gray.setStroke()
grayPath.stroke()
}
Which will produce this exact replica:

Show part of a circle

i want to display an arbitray part of a circle.
I know how to get a round View using layer.cornerRadius now i want to see only a part of that circle(for any given radiant value). It would be ok, if the rest of it would be simply hidden beneath something white.
Any ideas, how to achieve that?
edit:
i have written a class for my View:
class Circle:UIView {
var rad = 0
let t = CGFloat(3.0)
override func draw(_ rect: CGRect) {
super.draw(rect)
let r = self.frame.width / CGFloat(2)
let center = CGPoint(x: r, y: r)
let path = UIBezierPath()
path.move(to: CGPoint(x: t, y: r))
path.addLine(to: CGPoint(x: 0.0, y: r))
path.addArc(withCenter: center, radius: CGFloat(r), startAngle: CGFloat(Double.pi), endAngle: CGFloat(Double.pi+rad), clockwise: true)
let pos = path.currentPoint
let dx = r - pos.x
let dy = r - pos.y
let d = sqrt(dx*dx+dy*dy)
let p = t / d
path.addLine(to: CGPoint(x: pos.x + p * dx, y: pos.y + p * dy))
path.addArc(withCenter: center, radius: r-t, startAngle: CGFloat(Double.pi+rad), endAngle: CGFloat(Double.pi), clockwise: false)
UIColor(named: "red")?.setFill()
path.fill()
}
}
public func setRad(perc:Double) {
rad = Double.pi * 2 * perc
}
in my view controller i call
circleView.layer.cornerRadius = circleView.frame.size.width / 2
circleView.clipsToBounds = true
circleView.layer.borderWidth = 1
circleView.layer.borderColor = UIColor.darkGray.cgColor
circleView.layer.shadowColor = UIColor.black.cgColor
circleView.layer.shadowOpacity = 1
circleView.layer.shadowOffset = CGSize.zero
circleView.layer.shadowRadius = 3
circleView.layer.masksToBounds = false
circleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.onTap)))
now i get the full square View with a black circle from the corner and the red arc that i draw. If necessary i will post a picture tomorrow
One way to do it is to draw a UIBezierPath.
If you just want an arc, you can call the init(arcCenter:radius:startAngle:endAngle:clockwise:) initializer. Remember to specify the start and end angles in radians!
After that, you can set the stroke color and call stroke
If you want a sector of a circle, you can create a UIBezierPath with the parameterless initializer, then move(to:) the center of the circle, and then addLine(to:) the start of the arc. You can probably calculate where this point is with a bit of trigonometry. After that, call addArc like I described above, then addLine(to:) the point where you started. After that, you can fill the path.

Swift Custom Pie Chart - Strange behavior cutting transparent circle from multiple UIBezierPaths

Creating a custom pie chart / doughnut style graph with Swift, and am running into a strange problem when trying to cut the hole out of the doughnut. I've tried variations on center and radius for the second UIBezierPath, but I haven't been able to accomplish a clean cut hole from the center. Any help would be greatly appreciated.
Subclass of UIView:
import UIKit
public class DoughnutView: UIView {
public var data: [Float]? {
didSet { setNeedsDisplay() }
}
public var colors: [UIColor]? {
didSet { setNeedsDisplay() }
}
#IBInspectable public var spacerWidth: CGFloat = 2 {
didSet { setNeedsDisplay() }
}
#IBInspectable public var thickness: CGFloat = 20 {
didSet { setNeedsDisplay() }
}
public override func draw(_ rect: CGRect) {
guard data != nil && colors != nil else {
return
}
guard data?.count == colors?.count else {
return
}
let center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
let radius = min(bounds.size.width, bounds.size.height) / 2.0
let total = data?.reduce(Float(0)) { $0 + $1 }
var startAngle = CGFloat(Float.pi)
UIColor.clear.setStroke()
for (key, value) in data!.enumerated() {
let endAngle = startAngle + CGFloat(2.0 * Float.pi) * CGFloat(value / total!)
let doughnut = UIBezierPath()
doughnut.move(to: center)
doughnut.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let hole = UIBezierPath(arcCenter: center, radius: radius - thickness, startAngle: startAngle, endAngle: endAngle, clockwise: true)
hole.move(to: center)
doughnut.append(hole)
doughnut.usesEvenOddFillRule = true
doughnut.close()
doughnut.lineWidth = spacerWidth
colors?[key].setFill()
doughnut.fill()
doughnut.stroke()
startAngle = endAngle
}
}
public override func layoutSubviews() {
super.layoutSubviews()
self.backgroundColor = UIColor.clear
setNeedsDisplay()
}
}
And then a ViewController:
class ViewController: UIViewController {
#IBOutlet weak var doughnut: DoughnutView!
override func viewDidLoad() {
super.viewDidLoad()
doughnut.data = [3, 14, 5]
doughnut.colors = [UIColor.red, UIColor.yellow, UIColor.blue]
view.backgroundColor = .purple
}
}
The result:
So you want this:
The problem is that addArc creates, well, an arc, not a wedge. That is, it creates a path that traces part of a circle, without radial segments going to or from the center of the circle. Since you haven't been careful adding those radial segments, when you call close(), you get straight lines where you don't want them.
I guess you're trying to add those radial segments with your move(to:) calls, but you haven't done everything necessary to make that work.
Anyway, this can be done more simply. Start with an arc tracing the outer edge of the slice, then add an arc tracing the inner edge of the slice in the opposite direction. UIBezierPath will automatically connect the end of the first arc to the start of the second arc with a straight line, and close() will connect the end of the second arc to the start of the first arc with another straight line. Thus:
let slice = UIBezierPath()
slice.addArc(withCenter: center, radius: radius,
startAngle: startAngle, endAngle: endAngle, clockwise: true)
slice.addArc(withCenter: center, radius: radius - thickness,
startAngle: endAngle, endAngle: startAngle, clockwise: false)
slice.close()
That said, we can improve your draw(_:) method in some other ways:
We can use guard to rebind data and colors to non-optionals.
We can also guard that data is not empty.
We can reduce radius by spacerWidth to avoid clipping the stroked borders. (You changed the stroke color to .clear in your question's code, but your image shows it as .white.)
We can use CGFloat uniformly to have fewer conversions.
We can divide total by 2π once instead of multiplying every value by 2π.
We can zip(colors, data) into a sequence instead of using enumerated() and subscripting colors.
Thus:
public override func draw(_ rect: CGRect) {
guard
let data = data, !data.isEmpty,
let colors = colors, data.count == colors.count
else { return }
let center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0)
let radius = min(bounds.size.width, bounds.size.height) / 2.0 - spacerWidth
let total: CGFloat = data.reduce(0) { $0 + CGFloat($1) } / (2 * .pi)
var startAngle = CGFloat.pi
UIColor.white.setStroke()
for (color, value) in zip(colors, data) {
let endAngle = startAngle + CGFloat(value) / total
let slice = UIBezierPath()
slice.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
slice.addArc(withCenter: center, radius: radius - thickness, startAngle: endAngle, endAngle: startAngle, clockwise: false)
slice.close()
color.setFill()
slice.fill()
slice.lineWidth = spacerWidth
slice.stroke()
startAngle = endAngle
}
}

setLineDash symmetrically in Swift 2/3

I'm developing an internet speed test app. Something I will do to practice and learn more about a future project.
This is my Swift Code:
import UIKit
#IBDesignable
class Arc: UIView {
#IBInspectable var dashWidth: CGFloat = 12.0
#IBInspectable var smallDashWidth: CGFloat = 5.0
override func draw(_ rect: CGRect) {
// Position and Radius of Arc
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
// Calculate Angles
let π = CGFloat(M_PI)
let startAngle = 3 * π / 4
let endAngle = π / 4
let radius = max(bounds.width / 1.15, bounds.height / 1.15) / 2 - dashWidth / 2
// Start Context
let context = UIGraphicsGetCurrentContext()
// MARK: Base
let arc = UIBezierPath(arcCenter: center, radius: radius,startAngle: startAngle,endAngle: endAngle,clockwise: true)
arc.addArc(withCenter: center, radius: radius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
arc.lineJoinStyle = .bevel
arc.lineCapStyle = .round
arc.close()
UIColor.yellow().setStroke()
arc.lineWidth = smallDashWidth
//context!.saveGState()
context!.setLineDash(phase: 0, lengths: [0, 0], count: 2)
arc.stroke()
context!.saveGState()
// MARK: dash Arc
let dashArc = UIBezierPath()
dashArc.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
// Round Line
dashArc.lineJoinStyle = .round;
// Set Stroke and Width of Dash
UIColor.white().setStroke()
dashArc.lineWidth = dashWidth
// Save Context and Set Line Dash
context!.saveGState()
context!.setLineDash(phase: 0, lengths: [2, 54], count: 2)
// Draw Line
dashArc.stroke()
// Restore Context
context!.restoreGState()
}
}
The result is this:
What I need to do:
I need to automate this line of code:
context!.setLineDash(phase: 0, lengths: [2, 54], count: 2)
The lengths are [2, 54] which numbers are added without calculation, only to get the final equation number taken to obtain this dynamically.
   
1: need to add dashes 12 (which may later be changed, being assigned as a variable) across the arc. The arc begins and ends at a variable angle (possibility to change later).
   
2: The value of dashArc.lineWidth = dashWidth can also be changed, and is an important item to calculate the space between the 12 dashes.
   
3: Since all the variables presented values can be variable, which is the best way to do this calculation.
   
4: The first and the last dash should be at the same angle that the respective startAngle and endAngle.
What I need:
I need a calculation that looks and spreads as symmetrically as possible the dashes during the arc.
I thought of a similar calculation to this:
var numberOfDashes = 12
var perimeterArc = ?
var widthDash = 2
spacing = (perimeterArc - (widthDash * numberOfDashes)) / numberOfDashes
context!.setLineDash(phase: 0, lengths: [widthDash, spacing], count: 2)
But I do not know how to calculate the perimeterArc.
Can someone help me? I could not think of anything to create a logical calculation for this in Swift 2/3.
I appreciate any tip.
Instead of trying to compute spaces directly by trying to use a dash pattern, first of all, think in terms of angles.
I lifted some code that was originally created as an example in my book PostScript By Example (page 281). I transliterated the postscript code to Swift (version 2, as version 3 can't seem to do anything useful).
I also eschewed your use of UIBezierPath mixed in with an access to the Graphics Context, since I have the feeling that there are some strange interactions between the two. UIBezierPath is intended to manage the Graphics Context under the covers.
Here is the code:
class ComputeDashes : UIView
{
let insetAmount: CGFloat = 40.0
let numberOfDashes: Int = 16
let startAngle: CGFloat = CGFloat(1.0 * M_PI / 4.0)
let endAngle: CGFloat = CGFloat(3.0 * M_PI / 4.0)
let subtendedAngle: CGFloat = CGFloat(2.0 * M_PI) - CGFloat(2.0 * M_PI / 4.0)
override init(frame: CGRect) {
super.init(frame: frame)
}
required init(coder: NSCoder) {
super.init(coder: coder)!
}
override func drawRect(rect: CGRect)
{
let insets: UIEdgeInsets = UIEdgeInsetsMake(insetAmount, insetAmount, insetAmount, insetAmount)
let newbounds: CGRect = UIEdgeInsetsInsetRect(self.bounds, insets)
let centre: CGPoint = CGPointMake(CGRectGetMidX(newbounds), CGRectGetMidY(newbounds))
let radius: CGFloat = CGRectGetWidth(newbounds) / 2.0
let context: CGContext = UIGraphicsGetCurrentContext()!
CGContextAddArc(context, centre.x, centre.y, radius, startAngle, endAngle, 1)
CGContextSetLineWidth(context, 10.0)
UIColor.magentaColor().set()
CGContextStrokePath(context)
// MARK: paint dashes
let innerRadius: CGFloat = radius * 0.75
CGContextSaveGState(context)
CGContextTranslateCTM(context, centre.x, centre.y)
let angle = subtendedAngle / CGFloat(numberOfDashes)
CGContextRotateCTM(context, endAngle)
for rot in 0...numberOfDashes {
let innerPoint: CGPoint = CGPointMake(innerRadius, 0.0)
CGContextMoveToPoint(context, innerPoint.x, innerPoint.y)
let outerPoint: CGPoint = CGPointMake(radius, 0.0)
CGContextAddLineToPoint(context, outerPoint.x, outerPoint.y)
CGContextRotateCTM(context, angle)
}
CGContextSetLineWidth(context, 2.0)
UIColor.blackColor().set()
CGContextStrokePath(context)
CGContextRestoreGState(context)
}
}
I believe that this approach is much more flexible, and avoids the tricky computations you would need to do to account for line widths in the 'on' phase of the dash pattern.
I hope this helps. Let me know what you think.