Blog

SwiftUI: Paths vs. Shapes

In SwiftUI, there are two similar ways to draw shapes:

  • A Path is a struct that contains the outline of a shape. It's very similar to the CGPath/CGMutablePath classes from Core Graphics.

  • A Shape is a protocol with a single requirement: a method path(in rect: CGRect) -> Path.

A path is a list of lines, curves and other segments that all have absolute positions. A shape doesn't know its size up front: the framework calls path(in:) once the final size is known. While a path is absolute, a shape can choose to adjust its path to the given rect.

To draw a resizable vector asset in code, we can create a Shape, and then use the given rect to draw an absolute path. Let's start with the code for an absolutely-sized balloon-like path:

								let balloon = Path { p in
    p.move(to: CGPoint(x: 50, y: 0))
    p.addQuadCurve(to: CGPoint(x: 0, y: 50),
	               control: CGPoint(x: 0, y: 0))
    p.addCurve(to: CGPoint(x: 50, y: 150),
               control1: CGPoint(x: 0, y: 100),
               control2: CGPoint(x: 50, y: 100))
    p.addCurve(to: CGPoint(x: 100, y: 50),
               control1: CGPoint(x: 50, y: 100),
               control2: CGPoint(x: 100, y: 100))
    p.addQuadCurve(to: CGPoint(x: 50, y: 0),
	               control: CGPoint(x: 100, y: 0))
}

balloon.boundingRect // (0, 0, 100, 150)

							

Interestingly, because Path conforms to View, we can directly draw this in a SwiftUI application:

								struct ContentView: View {
    var body: some View {
        balloon
    }
}

							

By default, the path is filled with the current foreground color. Like a CGPath, we can also apply an affine transform to the path, giving us a new Path:

								let balloon2x: Path = balloon.applying(CGAffineTransform(scaleX: 2, y: 2))
balloon2x.boundingRect // (0, 0, 200, 300)

							

By using the Shape protocol, we can have much more control over paths. For example, we could make a Balloon shape that automatically fills the entire rect (for simplicity, we ignore the rect's origin):

								struct Balloon: Shape {
    func path(in rect: CGRect) -> Path {
        let bounds = balloon.boundingRect
		let scaleX = rect.size.width/bounds.size.width
		let scaleY = rect.size.height/bounds.size.height
        return balloon.applying(CGAffineTransform(scaleX: scaleX, y: scaleY))
    }
}

							

You can easily turn this into a more generic Fit struct that takes the path as a parameter.

The other APIs are very similar between Path and Shape. For example, we can stroke a Path or a Shape, which gives us back either a new Path or a new Shape, respectively. The moment we apply a fill to the path or shape, the resulting type is some View, which means that we "lose" the fact that we were dealing with a path or shape. For example, it's not possible to apply a fill and then a stroke, as stroke is only available on paths and shapes.

In conclusion: paths and shapes are almost the same. When you need an absolute drawing, you can use either one, but when you want to adjust the path to the available size, use a Shape.

Swift Talk Episode 164 explores SwiftUI paths and shapes in more depth, as we build an animated loading indicator for our Swift Talk app. The first episode is free to watch.

To watch the entire SwiftUI collection, subscribe here.

  • Watch the Full Episode

  • See the Swift Talk Collection

Stay up-to-date with our newsletter or follow us on Twitter .

Back to the Blog

Recent Posts