Why does CGFloat casted from Float not exhibit CGFloat behavior? - swift

I have this simple example where I try to draw a circle. This code below does not give me a circle.
import UIKit
class PlayingCardView: UIView {
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext(){
context.addArc(center: CGPoint(x: bounds.midX,
y: bounds.midY), radius: 100.0,
startAngle: 0,
endAngle: CGFloat(2.0*Float.pi),
clockwise: true)
context.setLineWidth(5.0)
UIColor.red.setStroke()
context.strokePath()
print(2.0*CGFloat.pi)
print(CGFloat(2.0*Float.pi))
}
}
}
This is what I get with the above code:
with output:
6.283185307179586
6.283185005187988
from the print statements which correspond to 2.0*CGFloat.pi and CGFloat(2.0*Float.pi) respectively.
Updating the code to this (I only change the endAngle in context.addArc to be 2.0*CGFloat.pi instead of CGFloat(2.0*Float.pi)
import UIKit
class PlayingCardView: UIView {
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext(){
context.addArc(center: CGPoint(x: bounds.midX,
y: bounds.midY), radius: 100.0,
startAngle: 0,
endAngle: 2.0*CGFloat.pi,
clockwise: true)
context.setLineWidth(5.0)
UIColor.red.setStroke()
context.strokePath()
print(2.0*CGFloat.pi)
print(CGFloat(2.0*Float.pi))
}
}
}
I get this drawing (The circle is there)
There is obviously a difference between the casted CGFloat from Float and CGFloat. Does anybody know what it is and why this behavior is useful in Swift?

On a 64-bit platform, CGFloat is (essentially) Double, a 64-bit floating point number, whereas Float is a 32-bit floating point number.
So 2.0*Float.pi is “2π with 32-bit precision”, and converting that to the 64-bit quantity CGFloat preserves the value, without increasing the precision.
That is why 2.0*CGFloat.pi != CGFloat(2.0*Float.pi). The former is “2π with 64-bit precision”, and is what you should pass to the drawing functions.
In your particular case, CGFloat(2.0*Float.pi) is a tiny bit smaller than 2.0*CGFloat.pi, so that only an invisibly short arc is drawn (from radians 0.0 to approximately -0.00000003).
For a full circle you can alternatively use
let radius: CGFloat = 100.0
context.addEllipse(in: CGRect(x: bounds.midX - radius, y: bounds.midY - radius,
width: 2.0 * radius, height: 2.0 * radius))
and avoid all rounding problems.

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.

Why is my SKShapeNode(s) reversed?

I'm training my SpritKit skills, but I can't figure out why my SKShapeNode are "reversed", I think I'm missing something.
I was trying example from this question: Question
So, I tried the example with 2 rounded corners, and another example which is a half-circle.
I don't understand why the two corners curved are not the one specified on the UIBezierPath object.
class GameScene: SKScene {
override func didMove(to view: SKView) {
let rectangle = CGRect(x: 0, y: 0, width: 400, height: 400)
let path = UIBezierPath(roundedRect: rectangle, byRoundingCorners: [.topLeft, .bottomRight], cornerRadii: CGSize(width: 50, height: 50))
let shape = SKShapeNode(path: path.cgPath, centered: true)
shape.lineWidth = 3
addChild(shape)
}
}
And why, my half circle are not oriented in the good direction either.
While in the documentation - Apple documentation - It says:
For example, specifying a start angle of 0 radians, an end angle of π radians, and setting the clockwise parameter to true draws the bottom half of the circle.
class GameScene: SKScene {
override func didMove(to view: SKView) {
let path = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: 200, startAngle: 0, endAngle: CGFloat(M_PI), clockwise: true)
let shape = SKShapeNode(path: path.cgPath, centered: true)
shape.lineWidth = 3
addChild(shape)
}
}
Finally, can some one explain me how the array of 'corners' is considered as a mask?

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.

How to draw a semi-circle using swift iOS 9 and on the basis of one value draw angle and fill that only part with color

I want to draw the two semi circles like this as shown in picture (the below one)
I'm trying it but did not get anything.
Tried some chart api and a few code to draw pie chart from stackoverflow but they need to edit and I don't know about Core Graphics.
I am working on Xcode 7.3.1 and iOS 9.
My Question is:
How do I draw a semi-circle which take one value and first convert that value to get its equivalent angle and then draw an arc of this angle and fill color in that part?
The below code runs in the XCode iOS Playground. It creates a custom UIView class and draws two pie slices. The start and end angle are specified in percent of a full circle.
You can easily extend it to display more or less slices depending on the data you have.
The drawRect method creates a bezier path that starts in the center, then adds an arc segment and finally closes the path so it can be filled.
Xcode 10.2.1/Swift 4.2 Update
class PieChart : UIView {
override func draw(_ rect: CGRect) {
drawSlice(rect: rect, startPercent: 0, endPercent: 50, color: .green)
drawSlice(rect: rect, startPercent: 50, endPercent: 75, color: .red)
}
private func drawSlice(rect: CGRect, startPercent: CGFloat, endPercent: CGFloat, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let startAngle = startPercent / 100 * CGFloat.pi * 2 - CGFloat.pi
let endAngle = endPercent / 100 * CGFloat.pi * 2 - CGFloat.pi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.close()
color.setFill()
path.fill()
}
}
let pieChart = PieChart(frame: CGRect(x: 0.0, y: 0.0, width: 300.0, height: 300.0))
pieChart.backgroundColor = .clear
An Unnamed Earlier Version
import UIKit
class PieChart : UIView {
override func drawRect(rect: CGRect) {
drawSlice(rect, startPercent: 0, endPercent: 50, color: UIColor.greenColor())
drawSlice(rect, startPercent: 50, endPercent: 75, color: UIColor.redColor())
}
private func drawSlice(rect: CGRect, startPercent: CGFloat, endPercent: CGFloat, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let startAngle = startPercent / 100 * CGFloat(M_PI) * 2 - CGFloat(M_PI)
let endAngle = endPercent / 100 * CGFloat(M_PI) * 2 - CGFloat(M_PI)
let path = UIBezierPath()
path.moveToPoint(center)
path.addArcWithCenter(center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.closePath()
color.setFill()
path.fill()
}
}
let pieChart = PieChart(frame: CGRect(x: 0.0, y: 0.0, width: 300.0, height: 300.0))
pieChart.backgroundColor = UIColor.clearColor()
My issue has been resolved.
We just need to do a little change in the first line to make it like as in picture's semicircle:
drawSlice(rect, startPercent: 0, endPercent: 50, color: UIColor.greenColor())
drawSlice(rect, startPercent: 0, endPercent: 25, color: UIColor.redColor())
Codo's code in Swift 3:
import UIKit
class PieChart : UIView {
override func draw(_ rect: CGRect) {
drawSlice(rect, startPercent: 0, endPercent: 50, color: .green)
drawSlice(rect, startPercent: 50, endPercent: 75, color: .red)
}
private func drawSlice(_ rect: CGRect, startPercent: CGFloat, endPercent: CGFloat, color: UIColor) {
let center = CGPoint(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
let radius = min(rect.width, rect.height) / 2
let startAngle = startPercent / 100 * CGFloat.pi * 2 - CGFloat.pi
let endAngle = endPercent / 100 * CGFloat.pi * 2 - CGFloat.pi
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.close()
color.setFill()
path.fill()
}
}
let pieChart = PieChart(frame: CGRect(x: 0.0, y: 0.0, width: 300.0, height: 300.0))
pieChart.backgroundColor = UIColor.clear

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()