Drawing with UIKit's UIBezierPath

Luiz Pedro Franciscatto Guerra
9 min readJul 25, 2022

--

This article’s banner, written in the center: “UIKit — UIBezierPath — Bézier Curves”
Part 2/5

This is the 2nd article in my current Bezier Path series. This article is focused on teaching exactly how to use the UIBezierPath object, and, more specifically, the curves.

Introduction

Here, I'll teach how to use the UIBezierPath object whilst drawing shapes, using this object drawing methods. This might be easier after you read the first article in this series: Bézier Fundaments for Swift.

This article will be divided into 5 parts:

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

So prepare yourself, drawing with swift won't be alien technology anymore!

Workplace

After creating a new, empty project, all I did was:

  • Changed the AccentColor to a custom red (#BE0606);
  • Deleted the ViewController.swift file and everything inside the main.storyboard file;
  • Added a Tab Bar Controller and connected 3 view controllers to it;
  • Created 3 new view controllers (SquareViewController, DShapeViewController, and BannerViewController), connected them to the storyboard screens and added labels to them (to differentiate the screens).
The app views' workflow, consisting of a tab bar with 3 views
App workflow

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

UIBezierPath initializers

When we try to create a UIBezierPath object, we find out that the object has lots of the initializers:

A print of the Xcode class initialization code for the UIBezierPath, consisting of various initializations
UIBezierPath initializers

What is important for us to know now:

  1. UIBezierPath()
    A normal UIBezierPath class, is empty and you can make everything from scratch — you'll probably end up using this;
  2. UIBezierPath(arcCenter:radius:startAngle:endAngle:clockwise:)
    A circle (which you can set his center, size, where it starts and ends, and if it is clockwise or not — this can be important for animations);
  3. UiBezierPath(ovalIn:)
    You can create an oval shape with this method;
  4. UiBezierPath(rect:)
    This one lets you create a rectangle;
  5. UiBezierPath(roundedRect:cornerRadii)
    A rectangle with rounded corners (passing the size of the rectangle and the corner radius);
  6. UiBezierPath(roundedRect:byRoundingCorners:cornerRadii)
    And this one you can set exactly which corners you want to be rounded.

We will use in this article the first initializer (the one without parameters) since I want to showcase how to use its functions first.

Drawing a simple shape

Our objective in this section it's to draw a simple red square (see next image) using the drawLine method.

A simple square colored in red
Simple red square

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

First things first, we need to start with the lifecycle of your view (creation and updates) by

  • Creating an attribute that will store our object, and
  • Calling the method that will set up our drawing in the viewDidLayoutSubviews method:
class SquareViewController: UIViewController {
var shape: CAShapeLayer?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
shape?.removeFromSuperlayer()
drawSquare()
}
}

This way, the app redraws every time the layoutSubviews method is invoked (like when the device is rotated). Depending on what you are doing, just repositioning your object is enough.

Now, let's start the drawSquare() function! Finally, we will start to code. To draw with UIBezierCurve, we will need to do the following:

  • Create and configure the CAShapeLayer
  • Create and draw with UIBezierPath
  • Pass the bezier path to the shape layer and add the layer to the view hierarchy

Let's start by setting up our CAShapeLayer: we will say where it's going to be positioned and which color we will be drawing with (I'm using the app tint color I modified, feel free to change for any color you want!):

func drawSquare() {
let shapeLayer = CAShapeLayer()
shapeLayer.position = self.view.center
shapeLayer.fillColor = UIColor.tintColor.cgColor
}

(Actually, I offsetted the shape position to be slightly to the left and top (-50 points), so the view will be exactly centred in the simulator. The code I shared in this article doesn't have it to keep it simple.)

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

A graph showcasing which positions each vertex of a square will be for the current example.

After this, we can set up our UIBezierPath object and draw with it, informing how we want to do it:

func drawSquare() {
/// ...
let bezierPath = UIBezierPath()
bezierPath.move(to: .zero)
bezierPath.addLine(to: CGPoint(x: 100, y: 0))
bezierPath.addLine(to: CGPoint(x: 100, y: 100))
bezierPath.addLine(to: CGPoint(x: 0, y: 100))
bezierPath.addLine(to: .zero)
bezierPath.close()
}

We move to a point and start adding lines for each different point. After that, I called the close 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.

After drawing, we need to pass to the layer object the path we drew and finish the function by saving it in an attribute.

func drawSquare() {
/// ...
shapeLayer.path = bezierPath.cgPath
self.view.layer.addSublayer(shapeLayer)
self.shape = shapeLayer
}

Hey, this wasn't bad at all! And we got a pretty neat screen too that works well both in portrait and landscape:

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

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

Cool! Let's go to our next view controller 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 red borders
Simple “D” shape with a red 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 point's 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 DShapeViewController, we will use pretty much the same code, but this time we will change the background, add a stroke, and, in the drawing stage, we will use the addCurve method. The drawing method will be like so (the difference will be in bold letters):

func drawDShape() {
let shapeLayer = CAShapeLayer()
shapeLayer.position = self.view.center
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 5
shapeLayer.strokeColor = drawingColor

// Start the bezier path object
let bezierPath = UIBezierPath()

// In this comment we will draw our custom shape
shapeLayer.path = bezierPath.cgPath
self.view.layer.addSublayer(shapeLayer)
self.shape = shapeLayer
}

Now that we have everything in place, 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.

An image of a simple graph showcasing where each point and control point should be in the code
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

Implementing it we would have something like so:

func drawDShape() {
bezierPath.move(to: .zero)
bezierPath.addCurve(
to: CGPoint(x: 0, y: 100),
controlPoint1: CGPoint(x: 75, y: 0),
controlPoint2: CGPoint(x: 75, y: 100))
bezierPath.addLine(to: .zero)
}

And running it in the simulator will give us the following screen:

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

If you need my complete 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 go code this article banner as our new app background! But… where to start? As always, let's break our problem into smaller parts.

The article banner design which consists of 2 custom shapes, one at the top and the other at the bottom
This article banner

As we can see, now we have 2 shapes to keep track of - the top and the bottom. In the BannerViewController, we will create the following lifecycle:

class BannerViewController: UIViewController {
var topShapeLayer: CAShapeLayer?
var botShapeLayer: CAShapeLayer?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
topShapeLayer?.removeFromSuperlayer()
botShapeLayer?.removeFromSuperlayer()
addTopDrawing()
addBotDrawing()
}
}

This way we are updating the view every time the ViewController updates the layout.

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 3 straight lines and 2 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.

Everything else in the drawing function stays the same (configuration, color and render). This time, for the point positions, we will use relative values. Click here to know what are they 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, our addTopDrawing drawing code will be like this:

func addTopDrawing () {
// ...
let rect = UIScreen.main.bounds
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),
controlPoint1: CGPoint(
x: rect.width-40,
y: rect.height*0.15625),
controlPoint2: CGPoint(
x: rect.width*3/5,
y: rect.height*0.09375))
path.addCurve(to: CGPoint(x: 0, y: 50),
controlPoint1: CGPoint(x: 10, y: 75),
controlPoint2: CGPoint(x: 0, y: 65))
path.addLine(to: .zero)

path.close()
// ...
}

Note that all the pre-coded values that are being used in the multiplications are the relative values I mentioned before. The addBotDrawing will also use them:

func addBotDrawing () {
// ...
let rect = UIScreen.main.bounds
let width = rect.width
let height = rect.height
path.move(to: CGPoint(x: 0, y: rect.height*0.6875)) path.addCurve(to: CGPoint(x: width-40, y: height-75),
controlPoint1: CGPoint(
x: 40,
y: height*0.84375),
controlPoint2: CGPoint(
x: width*2/5,
y: height*0.90625))

path.addCurve(to: CGPoint(x: width, y: height-50),
controlPoint1: CGPoint(
x: width-10,
y: height-75),
controlPoint2: 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.close()
// ...
}

Now, if we run the code:

Nice!

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

Conclusion

UIBezierPath is a really powerful way to draw using Swift. Although I haven't written anything about animations, it's easy to see that you can have moving paths without having to import them from other tools and use third-party packages. Of course, they are still useful, but for smaller things, this will work perfectly.

Furthermore, I want to link an amazing website that is currently down (but there is an internet archive of it) that have two really good in-depth articles about the CAShapeLayer and its animations:

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

--

--

Luiz Pedro Franciscatto Guerra

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