Create a custom ChatView using UIBezierPath - iOS
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.
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 Class
file and name it asChatViewController
- 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 view
by clicking on theIs initial view controller
in Attribute Inspector. And also set its the Custom Class property toChatViewController
in Identity Inspector.
- 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 viewinnerView
that will contain our contents & have linked its outlet in our ChatViewController.swift file.
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 :
move(to:)
Moves the path’s current point to the specified location, takes in — a destination point.addLine(to:)
Appends a straight line to the path, takes in — a destination point.addArc(withCenter: , radius: , startAngle: , endAngle: , clockwise: Bool)
Appends an arc to the path, takes in acenter
for the arc, aradius
of the arc, astart angle
in radians(π), anend angle
in radians(π) and adirection
of translation(clockwise/anti-clockwise).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:
- TopLine: from (0,0) to (x-4,0) using
path.addLine(to: )
- Top-right arc: origin at (x-4,4) with radius 4 using
path.addArc(: )
- RightLine: from (x,4) to (x,y-4)
- Bottom-right arc: origin at (x-4, y-4) with radius 4
- BottomLine: from (x-4,y) to (12,y)
- Bottom-left arc: origin at (12,y-4) with radius 4
- LeftLine: from (8,y-4) to (8,16)
- 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 aCAShapeLayer
object by which we can add extra layers to a view. When creating a CAShapeLayer object hereshapeLaer
, 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
andbounds.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
}
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."
}
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.