Stroking CGPath in swift - swift

I'm trying to add a stroke to `CGPath` in a macOS app with the code below. The issue is that the stroked path can't be filled properly. With the current method, I'm adding the original path on top of the stroked path. This causes the overlapping parts to remain unfilled.
func getPreviewPath(path:CGPath, strokeWidth: CGFloat) ->CGPath{
let window = NSWindow()
let context = NSGraphicsContext(window: window).cgContext
context.setLineCap(CGLineCap(rawValue: 0)!)
context.setLineWidth(strokeWidth)
context.setLineJoin(.bevel)
context.addPath(path)
context.replacePathWithStrokedPath()
context.addPath(path)
return context.path!
}
I've read that this is caused by the even-odd fill rule. But there doesn't seem to be a way to change this.

Here's a minimal example using the stroke() method:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 50, y: 100))
path.addLine(to: CGPoint(x: 80, y: 20))
path.addLine(to: CGPoint(x: 120, y: 20))
}
.stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .bevel))
.padding()
}
}
And this is the result:
Update
Her is a slightly more complex example. It first creates a path containing a stroked path. This path is then filled. The result is the same.
struct ContentView: View {
var body: some View {
Path(createPath())
.fill(.black)
.padding()
}
func createPath() -> CGPath {
let path = CGMutablePath()
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 50, y: 100))
path.addLine(to: CGPoint(x: 80, y: 20))
path.addLine(to: CGPoint(x: 120, y: 20))
return path.copy(strokingWithWidth: 12.0, lineCap: .round, lineJoin: .bevel, miterLimit: 10)
}
}

Related

How do I stop the padding modifier from messing up my dashed lines?

One of the views of my swiftui app require a dash line running through the background. I added this by creating a custom shape called MyLine that creates a path using the following code:
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
return path
I make it a dashed line using
.stroke(style: StrokeStyle(dash: [10]))
Finally, I add it to the blackground of my intended view using the background modifier:
.background(
MyLine()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [10]))
)
This all works great except for one problem: whenever ANY padding is added to the line the dashes become lopsided as shown below. Why is this happening and how do I fix it?
Before Padding:
After Padding
Minimum reproducible example:
import SwiftUI
struct MyLine: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
return path
}
}
struct MyLineTest: View{
var body: some View {
VStack{
MyLine()
.stroke(style: StrokeStyle(dash: [10]))
}.frame(height: 60)
.padding() //Remove this line to fix the dashes
}
}
struct MyLine_Previews: PreviewProvider {
static var previews: some View {
MyLineTest()
.foregroundColor(.black)
}
}
Just paste this code into an empty swift view file.

How to draw a curve path after straight lines in SwiftUI

What I need is to draw a curve like the one shown on the right side of the picture below.
struct DrawingPaths: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 300))
path.addLine(to: CGPoint(x: 430, y: 0))
path.addQuadCurve(to: CGPoint(x:430, y: 0), control: CGPoint(x: 300, y: 5))
}
.fill(.blue)
}
That is a quad curve with 2 control points.
struct DrawingPaths: View {
var body: some View {
Path { path in
//Top left
path.move(to: CGPoint(x: 0, y: 0))
//Left vertical bound
path.addLine(to: CGPoint(x: 0, y: 300))
//Curve
path.addCurve(to: CGPoint(x: 430, y: 200), control1: CGPoint(x: 175, y: 350), control2: CGPoint(x: 250, y: 80))
//Right vertical bound
path.addLine(to: CGPoint(x: 450, y: 0))
}
.fill(.blue)
.edgesIgnoringSafeArea(.top)
}
}

Draw Shape over another Shape

Problem :
I cannot draw a shape on another shape.
What I am trying to achieve :
Draw circles on the line.
Anyway, the circle is shifting the line. I didn't find a way to make it as swift UI seems relatively new. I am currently learning swift and I prefer swift UI rater than storyboard.
If circle and line are different struct, this is because I want to reuse the shape later on.
There is the code :
import SwiftUI
public var PointArray = [CGPoint]()
public var PointArrayInit:Bool = false
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
var centerCustom:CGPoint
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: 20, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct CurveCustomInit: Shape {
private var Divider:Int = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let xStep:CGFloat = DrawingZoneWidth / CGFloat(Divider)
let yStep:CGFloat = DrawingZoneHeight / 2
var xStepLoopIncrement:CGFloat = 0
path.move(to: CGPoint(x: 0, y: yStep))
for _ in 0...Divider {
let Point:CGPoint = CGPoint(x: xStepLoopIncrement, y: yStep)
PointArray.append(Point)
path.addLine(to: Point)
xStepLoopIncrement += xStep
}
PointArrayInit = true
return (path)
}
}
struct TouchCurveBasic: View {
var body: some View {
if !PointArrayInit {
Arc(startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true, centerCustom: CGPoint(x: 50, y: 400))
.stroke(Color.blue, lineWidth: 4)
CurveCustomInit()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
} else {
}
}
}
struct TouchCurveBasic_Previews: PreviewProvider {
static var previews: some View {
TouchCurveBasic()
}
}
There is what I get :
Here is an other way for you, you can limit the size of drawing with giving a frame or you can use the available size of view without limiting it or even you can use the current limit coming from parent and updated it, like i did on drawing the Line. The method that I used was overlay modifier.
struct ContentView: View {
var body: some View {
ArcView(radius: 30.0)
.stroke(lineWidth: 10)
.foregroundColor(.blue)
.frame(width: 60, height: 60)
.overlay(LineView().stroke(lineWidth: 10).foregroundColor(.red).frame(width: 400))
}
}
struct ArcView: Shape {
let radius: CGFloat
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: Angle(degrees: 0.0), endAngle: Angle(degrees: 360.0), clockwise: true)
}
}
}
struct LineView: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLines([CGPoint(x: rect.minX, y: rect.midY), CGPoint(x: rect.maxX, y: rect.midY)])
}
}
}
result:

How to fill a path with a radial gradient in SwiftUI

I'm trying to fill a path with a radial gradient, but no matter what I try, it always ends up being one solid color (as in the image below). It should transition from red to blue.
How do you fill a path with a radial gradient in SwiftUI? What am I doing wrong?
Thank you!
struct ContentView: View {
var body: some View {
Rectangle()
.foregroundColor(Color.black)
Path { path in
path.addArc(center: CGPoint(x: 200, y: 200), radius: 100, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true)
}
.fill(RadialGradient(
gradient: Gradient(colors: [.red, .blue]),
center: UnitPoint(x: 200.0, y: 200),
startRadius: 20.0,
endRadius: 100.0
))
.edgesIgnoringSafeArea(.all)
}
}
As it turns out, UnitPoint's x and y are values between 0 and 1. Some simple math solved the problem.
This works:
struct ContentView: View {
private let x: Double = 200.0
private let y: Double = 200.0
private let radius: CGFloat = 100.0
var body: some View {
GeometryReader { geo in
Rectangle()
.foregroundColor(Color.black)
Path { path in
path.addArc(center: CGPoint(x: self.x, y: self.y), radius: self.radius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true)
}
.fill(RadialGradient(
gradient: Gradient(colors: [.red, .blue]),
center: UnitPoint(
x: CGFloat(self.x / Double(geo.size.width)),
y: CGFloat(self.y / Double(geo.size.height))
),
startRadius: self.radius * 0.30,
endRadius: self.radius
))
}
}
}

How to change shape color on tap

I would like to implement the logic to change the shape color on tap. I tried to modify the example for changing the shape on tap as follows:
import SwiftUI
struct PathView: View {
#State private var insetAmount: CGFloat = 50
#State private var fillColor: UIColor = UIColor.white
var body: some View {
GeometryReader {
geometry in
ZStack {
Trapezoid(insetAmount: insetAmount, fillColor: fillColor)
.frame(width: 200, height: 100)
.onTapGesture {
self.insetAmount = CGFloat.random(in: 10...90)
let demoColors = [UIColor.blue, UIColor.green, UIColor.red]
self.fillColor = demoColors.randomElement() ?? UIColor.white
}
}.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
}
}
}
struct Trapezoid: Shape {
var insetAmount: CGFloat
var fillColor: UIColor
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY))
path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: 0, y: rect.maxY))
path.closeSubpath()
fillColor.setFill()
UIColor.white.setStroke()
path.fill()
return path
}
}
The path, however, is black and the line path.fill() displays a warning:
Result of call to 'fill(style:)' is unused
Does anyone know how to set the color property of shape and change it on tap?
A fill should be applied to Shape.
Here is possible solution (tested with Xcode 12)
struct PathView: View {
#State private var insetAmount: CGFloat = 50
#State private var fillColor: UIColor = UIColor.white
var body: some View {
GeometryReader {
geometry in
ZStack {
Trapezoid(insetAmount: insetAmount)
.fill(Color(fillColor)) // << here !!
.frame(width: 200, height: 100)
.onTapGesture {
self.insetAmount = CGFloat.random(in: 10...90)
let demoColors = [UIColor.blue, UIColor.green, UIColor.red]
self.fillColor = demoColors.randomElement() ?? UIColor.white
}
}.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
}
}
}
struct Trapezoid: Shape {
var insetAmount: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY))
path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: 0, y: rect.maxY))
path.closeSubpath()
return path
}
}