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 theCGPath
/CGMutablePath
classes from Core Graphics. -
A
Shape
is a protocol with a single requirement: a methodpath(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.