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
    }
}

2019 08 20 01

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)

2019 08 20 02

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))
    }
}

2019 08 20 03

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.

2019 08 20 04

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

  • Related Episodes

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

Back to the Blog

recent posts