Create a custom ChatView using UIBezierPath - iOS

Rohit Kumar
8 min readJun 6, 2023

--

Our ChatView made by using UIBezierPath on a frame of size 150*250 •••••••••• Our ChatView adjusting dynamically according to size of its contents

In this article, we will try to draw a custom chat message view and learn how it can done in iOS through code.

To design a User Interface, we can use various pre-built views but the problem comes where we need to draw a custom shaped view to meet our design requirements. This is where Apple’s provides UIBezierPath to draw shapes programmatically.

Our aim will be to create a message view that will look similar to the view that we see while chatting in WhatsApp. We will then populate our custom chat view with some dummy data to see the how we adjust the size of the view dynamically. In this article, we will first create our custom view and then use this in our project.

Pre-requisites: If you have a basic understanding about using bezierPath, then it will be easy to create any custom view. But, don’t worry, some basic concepts are also covered in this article.

Our ChatView after adding dummy data as content into it

Project SetUp

Before we jump to the working of UIBezierPath, let’s first setup our project.

  • Create a new blank project and create a new Cocoa Touch Classfile and name it as ChatViewController
  • open the Main.storyboard, remove the default NavigationView and UITableViewController and add a new UIViewController to the canvas (here, I named it as ChatViewController).
  • Make it the Root viewby clicking on the Is initial view controller in Attribute Inspector. And also set its the Custom Class property to ChatViewController in Identity Inspector.
ChatViewController added as root view and set custom class as of ChatViewController
  • After it is done, I have added some background similar to our WhatsApp in our ChatViewController using UIImageView (you can get these background anywhere on the web). And, added a view innerView that will contain our contents & have linked its outlet in our ChatViewController.swift file.
Background images and innerView added

Analysing Our Custom View

Our setup is ready and let’s now move to draw our custom view. To draw the shape of our view using a Bézier path, we will first look at some methods provided by Apple in UIBezierPath which we will be using to draw our shape :

  1. move(to:) Moves the path’s current point to the specified location, takes in — a destination point.
  2. addLine(to:) Appends a straight line to the path, takes in — a destination point.
  3. addArc(withCenter: , radius: , startAngle: , endAngle: , clockwise: Bool) Appends an arc to the path, takes in a center for the arc, a radius of the arc, a start anglein radians(π), an end angle in radians(π) and a direction of translation(clockwise/anti-clockwise).
  4. close() join the current position to the point of origin and close the figure by drawing a straight line(by default).

Before going into code, it’s better to understand our screen as a cartesian plane with origin(0,0) at top-left corner of screen.

  • Positive X-axis will be along the width of screen in right direction
  • Positive Y-axis will be along the depth of screen in downward direction
  • Negative X-axis will be along the width of screen in left direction
  • Negative Y-axis will be along the upward direction

To start drawing our shapes, it may seem complex at first but it is quite simple to draw custom shapes. It’s all the use of geometry here. We will transverse from the top-left corner and move in clockwise direction and the steps are:

  1. TopLine: from (0,0) to (x-4,0) using path.addLine(to: )
  2. Top-right arc: origin at (x-4,4) with radius 4 using path.addArc(: )
  3. RightLine: from (x,4) to (x,y-4)
  4. Bottom-right arc: origin at (x-4, y-4) with radius 4
  5. BottomLine: from (x-4,y) to (12,y)
  6. Bottom-left arc: origin at (12,y-4) with radius 4
  7. LeftLine: from (8,y-4) to (8,16)
  8. SlantLine: from (8,16) to (0,0) using path.close()

Code our CustomView

Since, we have understand and analyse our custom chat view, let’s dive into code and define a new class ChatView of type UIView in our ChatViewController.swift file.

A bezier path produced by the UIBezierPath Class cannot stand alone. It requires a Core Graphics context to be rendered to. We can get the context by either CGContext, or overriding the draw(_:), or using special layers called CAShapeLayer objects.

In most of the articles on UIBezierPath, one can draw shapes for a custom view by overriding the draw(_:) method of the UIView class. But, here, we will create our Bezier path and add it to a new CAShapeLayer when the view is initialised.

class ChatView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

override func layoutSubviews() {
super.layoutSubviews()
let path = getBezierPath(width: bounds.maxX, height: bounds.maxY)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
layer.mask = shapeLayer
}

private func getBezierPath(width: Double, height: Double) -> UIBezierPath {
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))

// TopLine
path.addLine(to: CGPoint(x: width - 4, y: 0))

// Top-right arc
path.addArc(
withCenter: CGPoint(x: width - 4, y: 4),
radius: 4,
startAngle: CGFloat(Double.pi * 3 / 2),
endAngle: CGFloat(0),
clockwise: true
)

// RightLine
path.addLine(to: CGPoint(x: width, y: height - 4))

// Bottom-right arc
path.addArc(
withCenter: CGPoint( x: width - 4, y: height - 4),
radius: 4, startAngle: CGFloat(0),
endAngle: CGFloat(Double.pi / 2),
clockwise: true
)

// BottomLine
path.addLine(to: CGPoint(x: 12, y: height))

// Bottom-left arc
path.addArc(
withCenter: CGPoint(x: 12, y: height - 4),
radius: 4,
startAngle: CGFloat(Double.pi / 2),
endAngle: CGFloat(Double.pi),
clockwise: true
)

// LeftLine
path.addLine(to: CGPoint(x: 8, y: 16))

// SlantLine
path.close()
return path
}
}

The function getBezierPath(width: , height: ) takes the width and height parameter to generate the path geometry. The path is simply created using the basic geometric concepts.

In the layoutSubviews() function, we have created a CAShapeLayer object by which we can add extra layers to a view. When creating a CAShapeLayer object here shapeLaer, the shape, or more precisely its path must be specified. The simplest technique is to set that path is to first create a bezier path first, and then assign it to the shape layer object.

We can get the dynamic height and width for our custom view according to its content by using the bounds property of a view. The bounds of an UIView is the rectangle relative to its own coordinate system

bounds.maxX and bounds.maxY provides the width and height of our custom view, which we these values to create the geometry of our bezier path.

To give a custom shape to our ChatView, we have use the mask property. We set the mask of our ChatView to CAShapeLayer initialised object shapeLayer’s path.

Using the custom view class

At the end of our project setup steps, we have added an outlet for our innerView. Let’s dive into code for using ChatView in our ChatViewController.

override func viewDidLoad() {
super.viewDidLoad()

// scrollView
let scrollView = UIScrollView()
innerView.addSubview(scrollView)

// scrollView Constraints
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.topAnchor.constraint(equalTo: innerView.topAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: innerView.rightAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: innerView.bottomAnchor).isActive = true
scrollView.leftAnchor.constraint(equalTo: innerView.leftAnchor).isActive = true

// stackView
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fill
stackView.spacing = 8
scrollView.addSubview(stackView)

// stackView Constraints
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 24).isActive = true
stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
stackView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 16).isActive = true
stackView.widthAnchor.constraint(equalToConstant: 250).isActive = true

}

In the func viewDidLoad() we have added a ScrollView in our innerView and added it’s constraints, a vertical stackView inside scrollView(it’s optional to add stackView, I have added just to display multiple chats at a time).

override func viewDidLoad() {
... // continuation to above code

let chatView = ChatView(frame: .zero)
stackView.addArrangedSubview(chatView)
chatView.leftAnchor.constraint(equalTo: stackView.leftAnchor).isActive = true
chatView.rightAnchor.constraint(equalTo: stackView.rightAnchor).isActive = true

// image added inside ChatView
let imageView = UIImageView(image: UIImage(named: "flower"))
chatView.addSubview(imageView)
imageView.layer.masksToBounds = true
imageView.layer.cornerRadius = 4

// image constraints
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.topAnchor.constraint(equalTo: chatView.topAnchor, constant: 4).isActive = true
imageView.rightAnchor.constraint(equalTo: chatView.rightAnchor, constant: -4).isActive = true
imageView.bottomAnchor.constraint(equalTo: chatView.bottomAnchor, constant: -4).isActive = true
imageView.leftAnchor.constraint(equalTo: chatView.leftAnchor, constant: 12).isActive = true
imageView.heightAnchor.constraint(equalToConstant: 150).isActive = true
}
A flower image added inside of ChatView

We can also customise the inner contents of our ChatView as per our need. Now, in the below code snippet, I have in taken another ChatView instance as chatView1 and added it to the stackView.

Here, I have added a stackView inside chatView1 named innerStackView. Inside of this innerStackView, I have added a image and a label inside it and set proper constraints to them.

override func viewDidLoad() {
... // continuation to above code

let chatView1 = ChatView(frame: .zero)
stackView.addArrangedSubview(chatView1)
chatView1.leftAnchor.constraint(equalTo: stackView.leftAnchor).isActive = true
chatView1.rightAnchor.constraint(equalTo: stackView.rightAnchor).isActive = true

// a stackView inside chatView1
let innerStackView = UIStackView()
innerStackView.axis = .vertical
innerStackView.distribution = .fill
innerStackView.spacing = 4
chatView1.addSubview(innerStackView)

innerStackView.translatesAutoresizingMaskIntoConstraints = false
innerStackView.topAnchor.constraint(equalTo: chatView1.topAnchor, constant: 4).isActive = true
innerStackView.bottomAnchor.constraint(equalTo: chatView1.bottomAnchor, constant: -4).isActive = true
innerStackView.leftAnchor.constraint(equalTo: chatView1.leftAnchor, constant: 12).isActive = true
innerStackView.rightAnchor.constraint(equalTo: chatView1.rightAnchor, constant: -4).isActive = true

let imageView1 = UIImageView(image: UIImage(named: "dog"))
innerStackView.addArrangedSubview(imageView1)
imageView1.layer.masksToBounds = true
imageView1.layer.cornerRadius = 4
imageView1.translatesAutoresizingMaskIntoConstraints = false
imageView1.rightAnchor.constraint(equalTo: innerStackView.rightAnchor).isActive = true
imageView1.leftAnchor.constraint(equalTo: innerStackView.leftAnchor).isActive = true
imageView1.heightAnchor.constraint(equalToConstant: 150).isActive = true

let label1 = UILabel()
label1.numberOfLines = 0
label1.textColor = UIColor.darkGray
label1.font = UIFont.systemFont(ofSize: 13.0)
innerStackView.addArrangedSubview(label1)
label1.translatesAutoresizingMaskIntoConstraints = false
label1.rightAnchor.constraint(equalTo: innerStackView.rightAnchor).isActive = true
label1.leftAnchor.constraint(equalTo: innerStackView.leftAnchor).isActive = true
label1.text = "Dogs have been loyal companions to humans for thousands of years, providing unwavering love, loyalty, and friendship. Often referred to as man's best friend, dogs have ingrained themselves into our lives and have become an integral part of our families. This essay explores the various aspects that make dogs remarkable creatures, highlighting their intelligence, loyalty, and the positive impact they have on human lives."
}
Multiple ChatView added inside a stackView

Summary

So, our custom view ChatView is now ready and it looks awesome. We can use it anywhere and play around with it to know what more can be done. As we come to the end, I hope that everyone found the information in this article to be helpful and enlightening.

For reading references — I have found this articles very useful and informative.

--

--

Rohit Kumar
Rohit Kumar

Written by Rohit Kumar

SDE @Yellow.ai | Swift, UIKIt, SwiftUI

No responses yet