Exploring Path in SwiftUI

DevTechie Inc
Jun 24, 2022


Photo by Lili Popper on Unsplash

Path is used to build custom shapes and outlines. In this article, we will explore path.

At its simplest form path can be created with a simple initializer as shown below. We first move path at the top left, next we will add 4 lines at various different points.

struct PathExplorer: View {
    var body: some View {
        Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: 100, y: 500))
            path.addLine(to: CGPoint(x: 150, y: 200))
            path.addLine(to: CGPoint(x: 250, y: 500))
            path.addLine(to: CGPoint(x: 300, y: 0))
        }
        .stroke(Color.pink, lineWidth: 10)
    }
}
Shape Protocol with Path
Path is commonly used with Shape protocol. We create shape by extending the Shape protocol as shown below. Only required function for Shape protocol is called path(in rect:)

struct WShape: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: 100, y: 500))
            path.addLine(to: CGPoint(x: 150, y: 200))
            path.addLine(to: CGPoint(x: 250, y: 500))
            path.addLine(to: CGPoint(x: 300, y: 0))
        }
    }
}
We can use this shape as shown below:

struct PathExplorer: View {
    var body: some View {
        WShape()
        .fill(Color.pink)
    }
}
Bounding Box in Path
Path has a property which has a property that contains the outer bounding rectangle of the path. Example below:

struct PathExplorer: View {
    var body: some View {
        EllipseInBox()
            .stroke()
    }
}struct EllipseInBox: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.addEllipse(in: rect)
            path.addRect(path.boundingRect)
        }
        .offsetBy(dx: 10, dy: 10)
        .applying(CGAffineTransform(scaleX: 0.9, y: 0.9))
    }
}
Ellipse Path
Lines are not the only thing you can create with path, path gives ability to add some built-in shapes as well with simple function calls. Let’s create a new shape called “EllipseShape”:

struct EllipseShape: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.addEllipse(in: rect)
        }
    }
}
We can use this shape just like before:

struct PathExplorer: View {
    var body: some View {
        EllipseShape()
        .fill(Color.pink)
    }
}
Rectangle Path
Similar to the ellipse path, we can add a rectangle path.

struct RectangleShape: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.addRect(rect)
        }
    }
}
Use of the Rectangle shape:

struct PathExplorer: View {
    var body: some View {
        VStack {
            RectangleShape()
                .fill(Color.red)
            
            RectangleShape()
                .fill(Color.orange)
            
            RectangleShape()
                .fill(Color.green)
        }
    }
}
Rounded Rectangle Shape
Path also provides a way to draw rounded rectangle. Rounded rectangle initializer for Path takes rect parameter along with corner size and corner style. Let’s create one to see this in action:

struct RoundedRectangleShape: Shape {
    func path(in rect: CGRect) -> Path {
        Path { path in
            path.addRoundedRect(in: rect, cornerSize: CGSize(width: 50, height: 50), style: .circular)
        }
    }
}
Use this inside a view:

struct PathExplorer: View {
    var body: some View {
        VStack {
            RoundedRectangleShape()
                .fill(Color.red)
            
            RoundedRectangleShape()
                .fill(Color.orange)
            
            RoundedRectangleShape()
                .fill(Color.green)
        }
    }
}
Path without closure
Path initializer we have been using so far is a closure that returns a path object but we don’t have to do that, we can use instance based approach to create path as well, as shown below. We will change our rounded rectangle shape implementation to following:

struct RoundedRectangleShape: Shape {
    func path(in rect: CGRect) -> Path {
        var path = Path()
        path.addRoundedRect(in: rect, cornerSize: CGSize(width: 50, height: 50), style: .continuous)
        return path
    }
}
Usage in view:

struct PathExplorer: View {
    var body: some View {
        VStack {
            RoundedRectangleShape()
                .fill(Color.red)
            
            RoundedRectangleShape()
                .fill(Color.orange)
            
            RoundedRectangleShape()
                .fill(Color.green)
        }
    }
}
Path with CGPath
Path provides an initializer where you can convert CGPath into Path type.

CG in CGPath stands for Core Graphics, you can learn more about CGPath here in Apple’s documentation: https://developer.apple.com/documentation/coregraphics/cgpath

Let’s see that in action next:

struct CGPathShape: Shape {
    func path(in rect: CGRect) -> Path {
        let cgPath = CGPath(rect: rect.insetBy(dx: 20, dy: 20), transform: .none)
        return Path(cgPath)
    }
}
Let’s use this inside a view:

struct PathExplorer: View {
    var body: some View {
        VStack {
            CGPathShape()
                .fill(Color.red)
            
            CGPathShape()
                .fill(Color.orange)
            
            CGPathShape()
                .fill(Color.green)
        }
    }
}
Path using String
Path has another very cool initializer and this one takes a string to draw path. Because it takes a string to draw path meaning you can store this path string value in database and fetch it back to draw it on to the screen.

Let’s draw path using path string. Path format is

Move

x point space y point space m = move to point x and y

i.e: 100 100 m = move to point (100,100) on screen

Line

x point space y point space l = draw line to point x and y

i.e: 100 150 l = draw line to point (100, 150) on screen

Example in code:

struct PathExplorer: View {
    var pathInString = "100 100 m 150 100 l 100 150 l 150 150 l 100 100 l"
    
    var body: some View {
        HStack {
            Path(pathInString)!
                .stroke(style: StrokeStyle(lineWidth: 2))
        }
    }
}
With the previous example, you must be thinking how to get this pathString, well you can generate one by converting existing path into a string using description property of path as shown below:

struct PathExplorer: View {
    var body: some View {
        HStack {
            CGPathShape()
                .fill(.orange)
        }
    }
}struct CGPathShape: Shape {
    func path(in rect: CGRect) -> Path {
        let cgPath = CGPath(rect: rect.insetBy(dx: 20, dy: 20), transform: .none)
        let path = Path(cgPath)
        print(path.description)
        return path
    }
}
Console print: 20 20 m 300 20 l 300 830 l 20 830 l h

Path is Empty
If you are drawing path dynamically and you need to know the path object returned from shape is empty or not then you can use path:

Path object with valid path

struct PathExplorer: View {
    var pathInString = "100 100 m 150 100 l 100 150 l 150 150 l 100 100 l"
    
    var body: some View {
        HStack {
            if Path(pathInString)?.isEmpty == true {
                Text("What's going on DevTechie?")
            } else {
                Path(pathInString)!
                    .stroke(style: StrokeStyle(lineWidth: 2))
            }
        }
    }
}
Path object with invalid or empty value

struct PathExplorer: View {
    var pathInString = ""
    
    var body: some View {
        HStack {
            if Path(pathInString)?.isEmpty == true {
                Text("What's going on DevTechie?")
            } else {
                Path(pathInString)!
                    .stroke(style: StrokeStyle(lineWidth: 2))
            }
        }
    }
}
Path Contains Point
If you want to find out that path contains a point, you can perform that check using method called contains.

struct PathExplorer: View {
    var body: some View {
        let line = Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: 100, y: 500))
            path.addLine(to: CGPoint(x: 100, y: 200))
            path.closeSubpath()
        }
        
        print(line.contains(CGPoint(x: 100, y: 200))) //true
        
        return line.fill(.pink)
    }
}
StrokeStyle for Path
Path’s stroke can by styled using StrokeStyle. Let’s take an example for this:

struct PathExplorer: View {
    var body: some View {
        let line = Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: 100, y: 200))
            path.addLine(to: CGPoint(x: 100, y: 500))
            path.closeSubpath()
        }
        
        let style = StrokeStyle(lineWidth: 3.0,
                                lineCap: .round,
                                lineJoin: .round,
                                miterLimit: 3.0,
                                dash: [15.0, 15.0, 0.0],
                                dashPhase: 15.0)
        return line.stroke().stroke(style: style).fill(.orange)
    }
}
TrimmedPath
Path can be trimmed between 0 to 1 representing 0% to 100% trimming. Let’s take an example for that:

struct PathExplorer: View {
    var body: some View {
        let line = Path { path in
            path.move(to: .zero)
            path.addLine(to: CGPoint(x: 100, y: 200))
            path.addLine(to: CGPoint(x: 100, y: 500))
            path.closeSubpath()
        }
            .trimmedPath(from: 0.0, to: 0.7)
        
        let style = StrokeStyle(lineWidth: 3.0,
                                lineCap: .round,
                                lineJoin: .round,
                                miterLimit: 3.0,
                                dash: [15.0, 15.0, 0.0],
                                dashPhase: 15.0)
        return line.stroke().stroke(style: style).fill(.orange)
    }
}
Adding Curve in Path
We can use addCurve function to add a cubic Bézier curve to the path, with the specified end point and control points. This function takes three parameters,

point: point where curve will end.

cp1: first control point for curve.

cp2: second control point

struct CurvedPath: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 100, y: 100))
            path.addCurve(to: CGPoint(x: 100, y: 100),
                          control1: CGPoint(x: 100, y: 0),
                          control2: CGPoint(x: 0, y: 100))
            
            path.addCurve(to: CGPoint(x: 100, y: 100),
                          control1: CGPoint(x: 200, y: 100),
                          control2: CGPoint(x: 100, y: 200))
            
            path.addCurve(to: CGPoint(x: 100, y: 100),
                          control1: CGPoint(x: 10, y: 100),
                          control2: CGPoint(x: 100, y: 200))
            
            
            path.addCurve(to: CGPoint(x: 100, y: 100),
                          control1: CGPoint(x: 100, y: 0),
                          control2: CGPoint(x: 200, y: 100))
            
        }
        .fill(.green)
    }
}
Quadratic Bézier Curve Path
We can draw curve with quadratic Bézier curve to the path, with a specified end point and control point.

Let’s try out with an example:

struct CurvePath: View {
    var body: some View {
        Path { path in
            path.move(to: CGPoint(x: 100, y: 100))
            path.addQuadCurve(to: CGPoint(x: 100, y: 200),
                              control: CGPoint(x: 200, y: 200))
            path.addEllipse(in: CGRect(x: 10, y: 10, width: 200, height: 250))
            path.addEllipse(in: CGRect(x: 50, y: 70, width: 30, height: 30))
            path.addEllipse(in: CGRect(x: 150, y: 70, width: 30, height: 30))
        }
        .stroke(.orange)
    }
}


With that we have reached the end of this article. Thank you once again for reading. Subscribe to our weekly newsletter at https://www.devtechie.com