I am reading a random Path, and I am extracting it into its Path.Element components. Then, I am trying to reverse the process and join the extracted Path.Element components together to create the final path. Currently, my code works partly; it draws some parts of the path, but for others, it shows errors in the Xcode console like this:
CGPathAddCurveToPoint: no current point.
CGPathCloseSubpath: no current point.
CGPathAddLineToPoint: no current point.
I am not sure how to solve this issue. In the process, I have broken the path down into its components and, without changing the values, I am trying to glue all the pieces back together. However, these errors and issues keep occurring.
import SwiftUI
struct ContentView: View {
@State private var myCustomPathComponents: [CustomPath] = pathComponents(path: comprehensivePath)
@State private var myPath: Path = Path()
var body: some View {
VStack {
GeometryReader { proxy in
Color.white
ForEach(myCustomPathComponents) { item in
item.pathValue
.stroke(.black, lineWidth: 2.0)
}
}
GeometryReader { proxy in
Color.white
ForEach(myCustomPathComponents) { item in
myFunc(value: item.pathElements)
.stroke(Color.red, lineWidth: 2.0)
}
}
}
.padding()
.onAppear {
print("count:", myCustomPathComponents.count)
print("- - - - - - - - - - -")
for item in myCustomPathComponents {
print(item.pathValue.cgPath)
print("- - - - - - - - - - -")
}
}
}
}
let comprehensivePath: Path = {
var path = Path()
// Line
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 120, y: 20))
// Circle
path.addEllipse(in: CGRect(x: 20, y: 30, width: 100, height: 100))
// Rectangle
path.addRect(CGRect(x: 150, y: 30, width: 100, height: 50))
// Quadratic Curve
path.move(to: CGPoint(x: 250, y: 20))
path.addQuadCurve(to: CGPoint(x: 300, y: 120), control: CGPoint(x: 275, y: 0))
// Cubic Curve
path.move(to: CGPoint(x: 20, y: 150))
path.addCurve(to: CGPoint(x: 120, y: 250), control1: CGPoint(x: 50, y: 100), control2: CGPoint(x: 90, y: 200))
// Polygon (Triangle)
path.move(to: CGPoint(x: 250, y: 150))
path.addLine(to: CGPoint(x: 300, y: 150))
path.addLine(to: CGPoint(x: 275, y: 200))
path.closeSubpath()
// Arc (part of a circle)
path.move(to: CGPoint(x: 150, y: 200))
path.addArc(center: CGPoint(x: 150, y: 200), radius: 50, startAngle: .degrees(0), endAngle: .degrees(180), clockwise: true)
return path
}()
struct CustomPath: Identifiable {
let id: UUID = UUID()
var pathValue: Path
var pathElements: [Path.Element]
}
func pathComponents(path: Path) -> [CustomPath] {
var components: [CustomPath] = []
var currentPath = Path()
var currentElements: [Path.Element] = []
path.forEach { element in
currentElements.append(element)
switch element {
case .move(to: let point):
// Save the current path as a `CustomPath` before starting a new one
if !currentPath.isEmpty {
components.append(CustomPath(pathValue: currentPath, pathElements: currentElements))
currentPath = Path()
currentElements = []
}
currentPath.move(to: point)
case .line(to: let point):
currentPath.addLine(to: point)
case .quadCurve(to: let endPoint, control: let controlPoint):
currentPath.addQuadCurve(to: endPoint, control: controlPoint)
case .curve(to: let endPoint, control1: let control1, control2: let control2):
currentPath.addCurve(to: endPoint, control1: control1, control2: control2)
case .closeSubpath:
currentPath.closeSubpath()
components.append(CustomPath(pathValue: currentPath, pathElements: currentElements))
currentPath = Path()
currentElements = []
}
}
// Append any remaining path if it hasn't been added yet
if !currentPath.isEmpty {
components.append(CustomPath(pathValue: currentPath, pathElements: currentElements))
}
return components
}
func myFunc(value: [Path.Element]) -> Path {
var finalPath = Path()
for element in value {
switch element {
case .move(to: let point):
finalPath.move(to: point)
case .line(to: let point):
finalPath.addLine(to: point)
case .quadCurve(to: let endPoint, control: let controlPoint):
finalPath.addQuadCurve(to: endPoint, control: controlPoint)
case .curve(to: let endPoint, control1: let control1, control2: let control2):
finalPath.addCurve(to: endPoint, control1: control1, control2: control2)
case .closeSubpath:
finalPath.closeSubpath()
}
}
return finalPath
}
This is just a simple mistake in the pathComponents
function.
Consider a path with elements moveto, lineto, moveto, curveto, closepath
. The expected result of passing this into pathComponents
would result in the first 2 elements forming a CustomPath
, and the last 3 elements forming another. However, your algorithm actually groups the first 3 elements into a CustomPath
, and the last 2 elements into another.
Try stepping through the code with a debugger, and consider what happens when the forEach
reaches the second moveto
element. At the start of the iteration, currentElements
would contain the first two elements moveto
and lineto
. The second moveto
is then added to currentElements
, making it [moveto, lineto, moveto]
. Since !currentPath.isEmpty
is true, you end up creating a CustomPath
with [moveto, lineto, moveto]
as its pathElements
. The next iteration puts a curveto
as the first element of currentElements
. When it comes to drawing this second CustomPath
, you will end up calling addLine(to:)
without calling move(to:)
, so there is no current point.
A simple fix is:
path.forEach { element in
currentElements.append(element)
switch element {
case .move(to: let point):
if !currentPath.isEmpty {
components.append(CustomPath(
pathValue: currentPath,
pathElements: currentElements.dropLast() // drop the moveto we just added
))
currentPath = Path()
currentElements = [element] // move the moveto into the next path's currentElements
}
currentPath.move(to: point)
// the rest are the same...