Drawing With SwiftUI’s Path

How to use the SwiftUI’s Path view to draw lines, curves, and more complex figures

Luiz Pedro Franciscatto Guerra
Better Programming

--

This article’s banner, written in the center: “SwiftUI — Path — Bézier Curves”
Part 4/5

This is the fourth article in my current Bezier Path series. This article focuses on teaching you how to use the Path View, specifically, the curves.

Introduction

Here, I’ll teach how to use the Path view while drawing shapes, using this object drawing method. This might be easier after you read the first article in this series: Bézier Fundaments for Swift.

This article will be divided into five parts:

  1. Workplace (setting up the project)
  2. Drawing a simple shape (to understand what we need to do besides draw)
  3. Drawing our first curved shape
  4. Drawing this article banner!

So prepare yourself. Drawing with swift won’t be alien technology anymore!

Workplace

After creating an empty SwiftUI project, all I did was:

  • Changed the AccentColor to a custom blue color (#006A98);
  • Created three Swift files and three empty Views (SquareView, DShapeView, BannerView)
  • Created a tab bar in the ContentView.swift
var body: some View {
TabView {
SquareView()
.tabItem {
Label("Square", systemImage: "1.square.fill")
}
DShapeView()
.tabItem {
Label("D Shape", systemImage: "2.circle.fill")
}
BannerView()
.tabItem {
Label("Banner", systemImage: "pencil.circle.fill")
}
}
}

This way, we can easily see our three different implementations without deleting code.

Drawing a Simple Shape

Our objective in this section is to draw a simple blue square (see next image) using the drawLine method.

A simple square colored in blue
Simple blue square

In the first screen (SquareView), we will start with an empty class (we will not use the viewDidLoad method).

First things first, let's change our body content. We will add a NavigationView (just to add a title to the page), call our Path view, and add some modifiers: the color that the path will be filled, padding, and the navigation title.

var body: some View {
NavigationView {
Path { path in

}
.fill(Color.accentColor)
.padding(20)
.navigationTitle(Text("Square View"))
}
}

Now we have everything we need to start drawing! We can see exactly what we need to do with our next image. In this case, we will use positive values, and the screen works by going right or down every time we increase the X or Y value (respectively). So, our four points would have the following values:

(0,0) (100,0) (0,100) (100,100)
The shape points

Now, with our four points positions discovered, we can transform them into code. Inside our Path view, let's move to the first point and add lines until we reach the first point again.

path.move(to: .zero)
path.addLine(to: CGPoint(x: 100, y: 0))
path.addLine(to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: 0, y: 100))
path.addLine(to: .zero)
path.closeSubpath()

Also, don't forget to call the closeSubpath method. This is important to avoid weird artifacts but also important when you want to draw more than one shape/stroke that is not connected with the same object.

Hey, this was pretty easy! Let's see how it runs:

A print of an iPhone simulator showing the code running
Simulator with the square shape

If you need my full code, you can get it here, in my GitHub repo.

Cool! Let’s go to our next view and draw something with a curve!

Drawing Our First Curved Shape

Our first curved shape will be a simple “D” shape like the following image:

A simple D shape, with blue borders
Simple “D” shape with a blue stroke

This way, we can showcase one simple curved path to learn how it works exactly. Here, we will finally use the Bézier Curve method! Instead of just drawing a line to a given point, we need to pass the point we need to pass de destination point and two Control Points. But… what is a Control Point?

Curve example and its control points

I explain it better in this article (if you didn’t read it, go read it. This will make things easier!), but it’s a way to calculate where each point in the curve should be, using the two extra points as guides! We can see in the above image the origin and final point (big red circles) and its control points (small red circles). In the images below, we can see what we can achieve with the same curve while messing around with the control points' positions:

Different curves with different control points positions for demonstration purposes
Examples of different control point positions

OK, now that you have an idea of how it works, let’s code it! In our DShapeView, let's move our Path view inside a Shape! So the code will look something like this:

struct DShapeView: View {
var body: some View {
NavigationView {
DShape()
.stroke(Color.accentColor, lineWidth: 5)
.navigationTitle(Text("Banner Shape View"))
.frame(width: 100, height: 100)
.offset(x: 25, y: -50)
}
}
}
struct DShape: Shape {
func path(in rect: CGRect) -> Path {
//
}
}

Now that we have created a Shape and set up everything inside of our view's body, let’s find out which values we need for our points. Given we are imagining a frame 100x100 (width and height), we would end up with something like the following image:

(0,0) first point (75,0) first control point (0,100) second point (75,100) second control point
Curve point positions

To achieve our “D” shape, we will need to:

  • Move to the first point
  • Add a curve to the second point
  • Define our curve control points (top right and bottom right)
  • Add a line back to the first point

Let's implement this logic inside our path function:

Path { path in
path.move(to: .zero)
path.addCurve(
to: CGPoint(x: 0, y: 100),
control1: CGPoint(x: 75, y: 0),
control2: CGPoint(x: 75, y: 100))
path.addLine(to: .zero)
path.closeSubpath()
}

Note that we could use the rect parameter to ensure our shape stays consistent through different sizes. But for now, I'll use absolute values.

Let's run it in the simulator:

A print of an iPhone simulator showing the code running
Simulator with the “D” shape

If you need my full code, you can get it here, in my GitHub repo.

Nice! Let’s do something more challenging!

Drawing This Article Banner

OK, now that we have the fundamentals, let’s code this article banner as our new app background! But… where to start? As always, let’s break our problem into smaller parts.

As we can see, now we have two shapes to keep track of — the top and the bottom. In the BannerView, we will create the code:

The article banner design which consists of 2 custom shapes, one at the top and the other at the bottom
This article banner
struct BannerView: View {
var body: some View {
ZStack {
BannerShape()
.fill(Color.accentColor)
.ignoresSafeArea()
Text("Banner Shape")
.font(.largeTitle).bold()
.foregroundColor(Color.accentColor)
}
}
}
struct BannerShape: Shape {
func path(in rect: CGRect) -> Path {
let width = rect.width
let height = rect.height

return Path { path in
}
}

With this code, we are setting up a text in the middle of the screen, our new struct Shape, and its color in the View's body.

The banner and its lines, curves, points, and control points

Now, into coding the shapes. As we can see in the first top image, the banner is made of the same shape, with one of them being the mirrored one. The shape can be broken into three straight lines and two simple Bézier ones.

We can see then, to achieve our top shape, we need to:

  1. Move to the 1st (I‘ll use the top left)
  2. Add a line to the 2nd (top right)
  3. Add a line to the 3rd (center right)
  4. Add a big curve to the 4th point (top left)
  5. Add a small curve to the 5th point (also at the top left)
  6. Add a line back to the 1st point.

This time, for the point positions, we will use relative values. Click here to know what they are and how to calculate them. Summing up, we can treat the min and max positions of the view as 0 and 1, respectively, and multiply the value we obtained by the width or height to achieve the correct position on the app screen. This way, the calculations will work with any screen size.

So, let's start by adding in our path method some variables that will help us:

func path(in rect: CGRect) -> Path {
let width = rect.width
let height = rect.height
}

After that, I'll add the code for the top shape:

func path(in rect: CGRect) -> Path {
// ...
path.move(to: .zero)
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: rect.height*0.3125))
path.addCurve(to: CGPoint(x: 40, y: 75),
control1: CGPoint(
x: rect.width-40,
y: rect.height*0.15625),
control2: CGPoint(
x: rect.width*3/5,
y: rect.height*0.09375))
path.addCurve(to: CGPoint(x: 0, y: 50),
control1: CGPoint(x: 10, y: 75),
control2: CGPoint(x: 0, y: 65))
path.addLine(to: .zero)
path.closeSubpath()
}

Note that all the pre-coded values that are being used in the multiplications are the relative values I mentioned before. The bottom shape will also use them. Here’s the code:

func path(in rect: CGRect) -> Path {
// ...
path.move(to: CGPoint(x: 0, y: rect.height*0.6875))
path.addCurve(to: CGPoint(x: width-40, y: height-75),
control1: CGPoint(
x: 40,
y: height*0.84375),
control2: CGPoint(
x: width*2/5,
y: height*0.90625))
path.addCurve(to: CGPoint(x: width, y: height-50),
control1: CGPoint(
x: width-10,
y: height-75),
control2: CGPoint(
x: width,
y: height-65))
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.addLine(to: CGPoint(x: 0, y: rect.height*0.6875))
path.closeSubpath()
}

Now, if we run the code, we’ll get the following:

Nice!

If you need my full code, you can get it from my GitHub repo.

Conclusion

SwiftUI's Path code is by far the easiest one to use since you don't have to wait for context, add the filling at the right time and the styling on it is also amazingly clean. Although I haven’t written anything about animations, it’s easy to see that you can have moving paths without importing them from other tools and using third-party packages. Of course, they are still useful, but this will work perfectly for smaller things.

Furthermore, I want to link to the Apple's Path tutorial, which showcases how easy it is to write even more complex geometries.

Want to Connect?Feel free to comment here or send me a message on Twitter. The article code repository can be found here.

--

--

Software Engineer, native iOS and Flutter developer. I also enjoy learning about design, security, code smells and machine learning. He/him