Draw custom shapes with UIBezierPath — iOS

Ayush Singhi
9 min readJun 1, 2020
Source — Pintrest.com

Today we will try to draw custom shapes and views and try to find out more about drawings in iOS (Swift) and how that can be done through CODE.

For all the frontend developers, one of the most important parts of the development process is the implementation of User Interface. We can create a simple UI with a combination of pre-made graphics and in-built views in the Interface Builder, but this is not enough, there are often cases when devs need to draw custom shaped views programmatically to meet UI requirements, and if we are not able to draw those, it creates a problem.

This is where UIBezierPath comes in, it’s Apple’s way of helping devs draw on-screen programmatically. It may seem complex at first but it is quite simple to draw custom shapes.

To make it more familiar, we will draw a custom shaped view and define it in such a way that we can use it like any inbuilt UI element.

In this write-up we will touch upon the following topics:

  1. Analysis of Custom View
  2. UIBezierPath class
  3. Drawing view programmatically
  4. Use it in your storyboard/code

To begin with, the aim will be to make a custom Card View that will have rounded corners and a button slot in the bottom-center

Final Card View with a button in the custom button slot and rounded corners

Note that here we are also going to draw round edges programmatically, which can be normally done using cornerRadius property of CAShapeLayer for almost any view. We do this to make concepts clearer by working on common properties, which will be interesting and will give a peek into how Apple might be doing things under the hood!

When drawing custom views, it is always beneficial to first analyze and divide it into sections that we can easily draw as individual components and later merge them to create a complete view. Once this is done only then should we dive into coding the view. So we will do the same for our custom Card View.

Analyzing custom view

Before analyzing the view it is important that we understand how the views are drawn on the screen. Consider the screen to be a Cartesian Plane with origin — (0, 0) at the top-left corner of the screen. Along with —

  1. Positive X-axis is along the width of the screen in the right direction
  2. Positive Y-axis is along the height of the screen in the downward direction
  3. Negative X-axis is along the left direction from the left edge of the screen
  4. Negative Y-axis is in the upward direction from the top edge of the screen

So for our analysis, we will consider the same with origin — (0, 0) at the top-left corner of the view frame, and positive x in the right direction along the width of the view and positive y in the downward direction along with the height of the view.

Layout guide for the analysis of the view

Card View — Width = N, Height = H

Button Slot — Width = N/2, Height = 30

Thus the rectangular frame of our custom UI is from (0,0) to (N, H)

Broadly there are three types of sections in the UI drawing

1. Corner Arcs
2. Button Slot Arcs
3. Straight Lines

We will now traverse the view starting from the top-right corner and move in the clockwise direction and investigate various sections and define their origin and length.

  1. Topline — from (30, 0) to (N-30, 0)
  2. Top-right arc — the origin of arc at (N-30, 30) and radius 30
  3. Right line — from (N, 30) to (N, H-30–30)
  4. Bottom-right arc — the origin of arc at (N-30, H-30–30) and radius 30
  5. Bottom-right line — from (N-30, H-30) to (3N/4, H-30)
  6. Right-buttonSlot arc — the origin of arc at (3N/4–30, H-30) and radius 30
  7. Bottom-buttonSlot line — from (3N/4 –30, H) to (N/4+30, H)
  8. Left-buttonSlot arc — the origin of arc at (N/4+30, H-30) and radius 30
  9. Bottom-left line — from (N/4, H-30) to (30, H-30)
  10. Bottom-left arc — the origin of arc at (30, H-30–30) and radius 30
  11. Left line — from (0, H-30–30) to (0, 30)
  12. Top-left arc — the origin of arc at (30, 30) and radius 30

From the above traversal, it is clear that we can divide our card view into 12 individual sections and then combine them to form the complete view.
We can do this by using UIBezierPath, where we can create 12 subpaths and later combine them to form the complete CardView.

Thus it is time to take a deeper look into UIBezierPath

UIBezierPath

Before we start to code a pre-requisite that needs to be addressed is UIBezierPath class. Apple says-

A UIBezierPath object is a wrapper for a CGPathRef data type. Paths are vector-based shapes that are built using line and curve segments. You can use line segments to create rectangles and polygons, and you can use curve segments to create arcs, circles, and complex curved shapes.

This is what we need to create vector-based paths. With this class, we can define custom paths that describe any shape, and use those paths to achieve any custom result we want — simple or complex.

Two different ways this can be done are —

  1. Using the shape initializers that let us draw pre-defined shapes — init(rect:), init(ovalIn:) etc
  2. Using methods to add paths of different geometry as subpaths — addLine(to:), addArc(_:), append(:) etc

Note — A bezier path cannot stand on its own, it needs a Core Graphics context where it can be rendered. Context can be passed on in different ways. Here we will pass context by subclassing our custom view from UIView.

Time to dive into code

Since we now have the required knowledge about the UIBezierPath and have done some fair analysis of our card view we are now ready to dive into code and start defining our new custom view. For this

  1. Start a new Xcode project.
  2. Define the custom class CardView that will inherit from UIView in ViewController.swift as in the below code snippet.
  3. Draw a custom shape for the CardView by overriding the draw(_:) method of the UIView class.
  4. In the draw(_:) method, create a UIBezierPath and add subpaths to it one by one in the same order as discussed in the above analysis.
///
/// Custom card view class
///
class CardView : UIView
{
// init the view with a rectangular frame
override init(frame: CGRect)
{
super.init(frame: frame)
backgroundColor = UIColor.clear
}
// init the view by deserialisation
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
backgroundColor = UIColor.clear
}
/// override the draw(_:) to draw your own view
///
/// Default implementation - `rectangular view`
///
override func draw(_ rect: CGRect)
{
// Card view corner radius
let cardRadius = CGFloat(30)
// Button slot arc radius
let buttonSlotRadius = CGFloat(30)

// Card view frame dimensions
let viewSize = self.bounds.size
// Effective height of the view
let effectiveViewHeight = viewSize.height - buttonSlotRadius
// Get a path to define and traverse
let path = UIBezierPath()
// Shift origin to left corner of top straight line
path.move(to: CGPoint(x: cardRadius, y: 0))

// top line
path.addLine(to: CGPoint(x: viewSize.width - cardRadius, y: 0))
// top-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: cardRadius
),
radius: cardRadius,
startAngle: CGFloat(Double.pi * 3 / 2),
endAngle: CGFloat(0),
clockwise: true
)
// right line
path.addLine(
to: CGPoint(x: viewSize.width, y: effectiveViewHeight)
)

// bottom-right corner arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width - cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius,
startAngle: CGFloat(0),
endAngle: CGFloat(Double.pi / 2),
clockwise: true
)
// right half of bottom line
path.addLine(
to: CGPoint(x: viewSize.width / 4 * 3, y: effectiveViewHeight)
)
// button-slot right arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width / 4 * 3 - buttonSlotRadius,
y: effectiveViewHeight
),
radius: buttonSlotRadius,
startAngle: CGFloat(0),
endAngle: CGFloat(Double.pi / 2),
clockwise: true
)

// button-slot line
path.addLine(
to: CGPoint(
x: viewSize.width / 4 + buttonSlotRadius,
y: effectiveViewHeight + buttonSlotRadius
)
)
// button left arc
path.addArc(
withCenter: CGPoint(
x: viewSize.width / 4 + buttonSlotRadius,
y: effectiveViewHeight
),
radius: buttonSlotRadius,
startAngle: CGFloat(Double.pi / 2),
endAngle: CGFloat(Double.pi),
clockwise: true
)
// left half of bottom line
path.addLine(
to: CGPoint(x: cardRadius, y: effectiveViewHeight)
)
// bottom-left corner arc
path.addArc(
withCenter: CGPoint(
x: cardRadius,
y: effectiveViewHeight - cardRadius
),
radius: cardRadius,
startAngle: CGFloat(Double.pi / 2),
endAngle: CGFloat(Double.pi),
clockwise: true
)
// left line
path.addLine(to: CGPoint(x: 0, y: cardRadius))
// top-left corner arc
path.addArc(
withCenter: CGPoint(x: cardRadius, y: cardRadius),
radius: cardRadius,
startAngle: CGFloat(Double.pi),
endAngle: CGFloat(Double.pi / 2 * 3),
clockwise: true
)

// close path join to origin
path.close()
// Set the background color of the view
UIColor.gray.set()
path.fill()
}
}

In the above code snippet, we have used —

  1. path.move(to:) — to move to a new position without drawing on the canvas, takes in — a destination point.
  2. path.addLine(to:) — to draw a straight line, takes in — a destination point.
  3. path.addArc(:) — to draw an arc, takes in — a center for the arc, a radius of the arc, a start angle in radians(π), an end angle in radians(π) and a direction of translation(clockwise/anti-clockwise).
  4. path.close() — to join the current position to the point of origin and close the figure by drawing a straight line(by default).

Since now we have successfully defined our custom view class — CardView, it is now time to use it.

Using the custom view class

To use our custom view, for this we will now move to storyboard —

  1. Open Main.storyboard
  2. Remove the default NavigationView and UITableViewController and add a new UIViewController to the canvas.
  3. Make it the Root view by clicking on the Is initial view controller in Attribute Inspector.
Added a new UIViewController and set it as the initial view controller

Once that is done we can start adding UI elements into our UIViewController.

  1. Start by adding a new view — UIView to the canvas and rename it to cardView
  2. Set the constraints for the UIView as —
1. Safe Area.trailing = cardView.trailing + 16 //Trailing constraint
2. cardView.centerY = Safe Area.centerY //Vertical constraint
3. cardView.leading = Safe Area.leading + 16 //Leading constraint
4. cardView.height = 0.5 × height //Height constraint
Added auto layout constraints to the UIView

Now we have our view ready and setup

In the property inspector on the right panel add the class of our UIView to CardView.

Set the class of the UIView to CardView

With this, the storyboard setup is done.

We are Done! And that is all we had to do to make our custom view show on the screen. Now run your app and see our custom view. Similar to this —

Adding a button to the card view

To add a UIButton to the view

  1. Open Main.storyboard file and add a UIButton to the card view
  2. Set up the constraints for the button as —
1. bottom = Button.bottom + 5      //Bottom constraint
2. Button.centerX = centerX //Horizontal constrain
3. Button.width = 0.47 × width //button width constraint
4. height = 50 //button height constraint

Change the button appearance by changing the background color/tint color

Add button and its constraints

To make the corners of the button curve, create an IBOutlet of the button.

@IBOutlet private weak var button: UIButton!

and add the following line to viewDidLoad() in the ViewController class.

button.layer.cornerRadius = button.bounds.height / 2

This will curve the corner with a radius equal to half the height.

Run the app, we will have our desired view.

Final custom CardView with a button slot

So our custom Card View is now ready and it looks awesome! We can use it anywhere we want and play around with it to see what more can be done.

There is so much more to cover, as this is a huge topic but for the time being thank you for following along, I have tried to keep this as informational as possible.

Suggestions to look for further study and enhancement-

  1. Modify the CardView class to contain open properties that can be used to customize appearance such as — cornerRadius, backgroundColor, buttonSlotHeight, buttonPadding etc.
  2. Add the CardView programmatically into the view hierarchy as a subview.
  3. For reading references — I found this very informational and helpful — https://developer.apple.com/library/archive/documentation/2DDrawing/Conceptual/DrawingPrintingiOS/BezierPaths/BezierPaths.html

--

--