swiftswiftui

How to build Path from Path.Element in SwiftUI?


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.

enter image description here

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
}

Solution

  • 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...