setLineDash symmetrically in Swift 2/3 - swift

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.

Related

iOS Swift circle chart

Any suggestion how to implement the following progress chart for iOS Swift?
Just break this down into individual steps.
The first question is how to draw the individual tickmarks.
One way is to draw four strokes using a UIBezierPath:
a clockwise arc at the outer radius;
a line to the inner radius;
a counter-clockwise arc at the inner radius; and
a line back out to the outer radius.
Turns out, you can skip the two lines, and just add those two arcs, and then close the path and you’re done. The UIBezierPath will add the lines between the two arcs for you. E.g.:
let startAngle: CGFloat = 2 * .pi * (CGFloat(i) - 0.2) / CGFloat(tickCount)
let endAngle: CGFloat = 2 * .pi * (CGFloat(i) + 0.2) / CGFloat(tickCount)
// create path for individual tickmark
let path = UIBezierPath()
path.addArc(withCenter: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
// use that path in a `CAShapeLayer`
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = …
shapeLayer.strokeColor = UIColor.clear.cgColor
shapeLayer.path = path.cgPath
// add it to our view’s `layer`
view.layer.addSublayer(shapeLayer)
Repeat this for i between 0 and tickCount, where tickCount is 90, and you have ninety tickmarks:
Obviously, use whatever colors you want, make the ones outside your progress range gray, etc. But hopefully this illustrates the basic idea of how to use UIBezierPath to render two arcs and fill the shape for each respective tick mark with a specified color.
E.g.
class CircularTickView: UIView {
var progress: CGFloat = 0.7 { didSet { setNeedsLayout() } }
private var shapeLayers: [CAShapeLayer] = []
private let startHue: CGFloat = 0.33
private let endHue: CGFloat = 0.66
override func layoutSubviews() {
super.layoutSubviews()
shapeLayers.forEach { $0.removeFromSuperlayer() }
shapeLayers = []
let outerRadius = min(bounds.width, bounds.height) / 2
let innerRadius = outerRadius * 0.7
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let tickCount = 90
for i in 0 ..< tickCount {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = color(percent: CGFloat(i) / CGFloat(tickCount)).cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
let startAngle: CGFloat = 2 * .pi * (CGFloat(i) - 0.2) / CGFloat(tickCount)
let endAngle: CGFloat = 2 * .pi * (CGFloat(i) + 0.2) / CGFloat(tickCount)
let path = UIBezierPath()
path.addArc(withCenter: center, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addArc(withCenter: center, radius: innerRadius, startAngle: endAngle, endAngle: startAngle, clockwise: false)
path.close()
shapeLayer.path = path.cgPath
layer.addSublayer(shapeLayer)
shapeLayers.append(shapeLayer)
}
}
private func color(percent: CGFloat) -> UIColor {
if percent > progress {
return .lightGray
}
let hue = (endHue - startHue) * percent + startHue
return UIColor(hue: hue, saturation: 1, brightness: 1, alpha: 1)
}
}
Clearly, you will want to tweak as you see fit. Perhaps change the color algorithm. Perhaps have it start from 12 o’clock rather than 3 o’clock. Etc. The details are less important than groking the basic idea of how you add shape layers with paths to your UI.

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

Masking circle segments in swift

I'm creating a simple player app. There is a circle, that shows a progress of playing a song.
What is the best way to draw this circle in Swift and make a mask? I assume I can draw a 2 circles putting the width stroke to the thickness I want and without filling it. And the white one has to be masked according to some parameter. I don't have an idea, how to mask it in a proper way.
I came up with this solution recently:
class CircularProgressView: UIView {
private let floatPi = CGFloat(M_PI)
private var progressColor = UIColor.greenColor()
private var progressBackgroundColor = UIColor.grayColor()
#IBInspectable var percent: CGFloat = 0.11 {
didSet {
setNeedsDisplay()
}
}
#IBInspectable var lineWidth: CGFloat = 18
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let origo = CGPointMake(frame.size.width / 2, frame.size.height / 2)
let radius: CGFloat = frame.size.height / 2 - lineWidth / 2
CGContextSetLineWidth(context, lineWidth)
CGContextMoveToPoint(context, frame.width / 2, lineWidth / 2)
CGContextAddArc(context, origo.x, origo.y, radius, floatPi * 3 / 2, floatPi * 3 / 2 + floatPi * 2 * percent, 0)
progressColor.setStroke()
let lastPoint = CGContextGetPathCurrentPoint(context)
CGContextStrokePath(context)
CGContextMoveToPoint(context, lastPoint.x, lastPoint.y)
CGContextAddArc(context, origo.x, origo.y, radius, floatPi * 3 / 2 + floatPi * 2 * percent, floatPi * 3 / 2, 0)
progressBackgroundColor.setStroke()
CGContextStrokePath(context)
}
}
You just have to set a correct frame to it (via code or interface builder), and set the percent property.
This solution is not using mask or two circles, just two arcs, the first start at 12 o clock and goes to 2 * Pi * progress percent, and the other arc is drawn from the end of the previous arc to 12 o clock.
Important: the percent property has to be between 0 and 1!

Drawing a partial circle

I'm writing a program that will take a number between 0 and 1, and then spits out a circle (or arc I guess) that is completed by that much.
So for example, if 0.5 was inputted, the program would output a semicircle
if 0.1, the program would output a tiny little arc that would ultimately be 10% of the whole circle.
I can get this to work by making the angle starting point 0, and the angle ending point 2*M_PI*decimalInput
However, I need to have the starting point at the top of the circle, so the starting point is 3*M_PI_2 and the ending point would be 7*M_PI_2
I'm just having trouble drawing a circle partially complete with these new starting/ending points. And I'll admit, my math is not the best so any advice/input is appreciated
Here is what I have so far
var decimalInput = 0.75 //this number can be any number between 0 and 1
let start = CGFloat(3*M_PI_2)
let end = CGFloat(7*M_PI_2*decimalInput)
let circlePath = UIBezierPath(arcCenter: circleCenter, radius: circleRadius, startAngle: start, endAngle: end, clockwise: true)
circlePath.stroke()
I just cannot seem to get it right despite what I try. I reckon the end angle is culprit, unless I'm going about this the wrong way
The arc length is 2 * M_PI * decimalInput. You need to add the arc length to the starting angle, like this:
let circleCenter = CGPointMake(100, 100)
let circleRadius = CGFloat(80)
var decimalInput = 0.75
let start = CGFloat(3 * M_PI_2)
let end = start + CGFloat(2 * M_PI * decimalInput)
let circlePath = UIBezierPath(arcCenter: circleCenter, radius: circleRadius, startAngle: start, endAngle: end, clockwise: true)
XCPCaptureValue("path", circlePath)
Result:
Note that the path will be flipped vertically when used to draw in a UIView.
You can use this extension to draw a partial circle
extension UIBezierPath {
func addCircle(center: CGPoint, radius: CGFloat, startAngle: Double, circlePercentage: Double) {
let start = deg2rad(startAngle)
let end = start + CGFloat(2 * Double.pi * circlePercentage)
addArc(withCenter: center,
radius: radius,
startAngle: start,
endAngle: end,
clockwise: true)
}
private func deg2rad(_ number: Double) -> CGFloat {
return CGFloat(number * Double.pi / 180)
}
}
Example usage (you can copy paste it in a playground to see the result)
let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
view.backgroundColor = UIColor.green
let layer = CAShapeLayer()
layer.strokeColor = UIColor.red.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineWidth = 8
let path = UIBezierPath()
path.addCircle(center: CGPoint(x: 50, y: 50), radius: 50, startAngle: 270, circlePercentage: 0.87)
layer.path = path.cgPath
view.layer.addSublayer(layer)
view.setNeedsLayout()