Icons are essential in iOS development. They can be used to display navigational information throughout your application, many of which will be familiar to the user, such as the search and home icon, as they are similar throughout app design.

But what is the best way of delivering these icons to your users? We could create image assets, drag them into your project and set a UIImageView image property to that icon. This is simple, easy and painless.

Another option is to draw your icon using CAShapeLayer. This sounds like more work than the original option, but don’t tune out just yet because I am going to explain to you the advantages of drawing your assets using CAShapeLayers and UIBezierPaths.

  1. Your drawing is going to look crystal clear on any screen, no matter what screen resolution you are working with. You can draw your icon on 1x, 2x or future 5x screens and it will not degrade at all.
  2. If you have to change your icon color to indicate a selected state, this is as easy as one line of code setting the color of the CAShapeLayer.
  3. You are able to make adjustments to the size, line thickness, color and other properties with minimal code change as opposed to creating multiple assets for different sizes.
  4. If you have one image asset, that needs to also have a selected state. You there are two image assets. Each image asset will need to exist in 1x, 2x and 3x resolution. Now you have six image assets. Six image assets which are a variation of one image. This is not efficient and will increase your application bundle size, which means larger downloads every time your user updates your app.

Hopefully, now I have convinced you that drawing your icons is a good idea, let me now show you how easy it is to do!

Tutorial:

We are going to start with a single view application template. Inside the first view controller, we are going to put a UIView and two buttons. The two buttons are going to be used to cycle through our shapes that we will be drawing inside our UIView. Connect the UIView and two buttons up with some outlets and IBActions like I have in the picture below.

Screen Shot 2018-04-14 at 3.52.32 PM.png

I always like to deal with degrees instead of radians so I have used a small utils class with a top-level function to help me work in degrees. You can copy it below.


// MARK: Frameworks
import UIKit
// MARK: Helper Methods
func radians(from degrees: CGFloat) -> CGFloat {
return ((degrees * .pi) / 180)
}

view raw

Utils.swift

hosted with ❤ by GitHub

Now we are going to start drawing our shapes, all of our shapes are going to be created using UIBezierPaths. Our UIBezierPaths are going to be instantiated with a CGRect which is going to be the bounds of our UIView. Inside this coordinate space we are going to be manually drawing the lines, points, and arcs of the shapes. This requires a little bit of practice but is very rewarding once you get the hang of it. For now, you can just copy some of the shapes I have created for you.


// MARK: Frameworks
import UIKit
// MARK: UIBezierPath Methods
extension UIBezierPath {
convenience init(homeIn rect: CGRect) {
self.init()
let roofHeight: CGFloat = rect.height * 0.55
let roofIndent: CGFloat = rect.width * 0.15
let doorHeight: CGFloat = rect.height * 0.3
let doorWidth: CGFloat = rect.width * 0.06
move(to: CGPoint(x: rect.width * 0.5, y: lineWidth))
addLine(to: CGPoint(x: rect.width – lineWidth, y: roofHeight))
addLine(to: CGPoint(x: rect.width – lineWidth – roofIndent, y: roofHeight))
addLine(to: CGPoint(x: rect.width – lineWidth – roofIndent, y: rect.height – lineWidth))
addLine(to: CGPoint(x: (rect.width * 0.5) + doorWidth, y: rect.height – lineWidth))
addLine(to: CGPoint(x: (rect.width * 0.5) + doorWidth, y: rect.height – doorHeight))
addLine(to: CGPoint(x: (rect.width * 0.5) – lineWidth – doorWidth, y: rect.height – doorHeight))
addLine(to: CGPoint(x: (rect.width * 0.5) – lineWidth – doorWidth, y: rect.height – lineWidth))
addLine(to: CGPoint(x: roofIndent, y: rect.height – lineWidth))
addLine(to: CGPoint(x: roofIndent, y: roofHeight))
addLine(to: CGPoint(x: lineWidth, y: roofHeight))
close()
}
convenience init(searchIn rect: CGRect) {
self.init()
let circleRadius: CGFloat = rect.width * 0.35
addArc(withCenter: CGPoint(x: circleRadius + lineWidth, y: circleRadius + lineWidth),
radius: circleRadius,
startAngle: 0,
endAngle: radians(from: 360),
clockwise: true)
move(to: CGPoint(x: 2*(circleRadius – lineWidth), y: 2*(circleRadius – lineWidth)))
addLine(to: CGPoint(x: rect.width, y: rect.height))
close()
}
convenience init(addIn rect: CGRect) {
self.init()
move(to: CGPoint(x: rect.width * 0.5, y: 0))
addLine(to: CGPoint(x: rect.width * 0.5, y: rect.height))
move(to: CGPoint(x: 0, y: rect.height * 0.5))
addLine(to: CGPoint(x: rect.width, y: rect.height * 0.5))
close()
}
convenience init(profileIn rect: CGRect) {
self.init()
let smallCircleRadius: CGFloat = rect.width * 0.25
let largeCircleRadius: CGFloat = rect.width * 0.5
addArc(withCenter: CGPoint(x: rect.width * 0.5, y: smallCircleRadius + lineWidth),
radius: smallCircleRadius,
startAngle: 0,
endAngle: radians(from: 360),
clockwise: true)
move(to: CGPoint(x: 0, y: rect.height))
addArc(withCenter: CGPoint(x: rect.width * 0.5, y: rect.height),
radius: largeCircleRadius – 2*lineWidth,
startAngle: radians(from: 180),
endAngle: radians(from: 0),
clockwise: true)
move(to: CGPoint(x: rect.width, y: rect.height))
close()
}
}

Now we just have to add the code to our UIViewController to show our UIBezierPaths as CAShapeLayers. Here you can see that we are instantiating an array of CAShapeLayers which is going to contain all of our shapes.

We have an index which we will be modifying using our two buttons, previous and next. Every time the index changes, our property observer, didSet, on the index will change the current shape being displayed inside our UIView, called shapeView.

If we wish to modify the stroke color, fill color, stroke width of the icons we can do so inside the shapeCreator helper function.


// MARK: Frameworks
import UIKit
// MARK: ViewController
class ShapeViewController: UIViewController {
// MARK: Outlets
@IBOutlet var shapeView: UIView!
@IBOutlet var buttonView: UIView!
@IBOutlet var nextButton: UIButton!
@IBOutlet var previousButton: UIButton!
// MARK: Variables
var shapes: [CAShapeLayer] = []
var index: Int = 0 {
didSet {
let currentShape = shapes[index]
shapeView.layer.sublayers?.removeAll()
shapeView.layer.addSublayer(currentShape)
}
}
// MARK: View Methods
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
shapes = [shapeCreator(bezierPath: UIBezierPath(homeIn: shapeView.bounds)),
shapeCreator(bezierPath: UIBezierPath(addIn: shapeView.bounds)),
shapeCreator(bezierPath: UIBezierPath(profileIn: shapeView.bounds)),
shapeCreator(bezierPath: UIBezierPath(searchIn: shapeView.bounds))]
index = 0
}
// MARK: Helper Methods
private func shapeCreator(bezierPath: UIBezierPath) -> CAShapeLayer {
let shape = CAShapeLayer()
shape.frame = bezierPath.bounds
shape.path = bezierPath.cgPath
shape.fillColor = UIColor.clear.cgColor
shape.strokeColor = UIColor.red.cgColor
shape.lineWidth = 2
return shape
}
// MARK: Action Methods
@IBAction func previous(_ sender: Any?) {
if index == 0 {
index = (shapes.count – 1)
} else {
index = index – 1
}
}
@IBAction func next(_ sender: Any?) {
if index == (shapes.count – 1) {
index = 0
} else {
index = index + 1
}
}
}

When you build and run your project you should see something similar to this screenshot. You should be able to cycle through a selection of four icons. I would encourage you to have a play around with UIBezierPaths to see what icons you can create inside your applications using CAShapeLayers.

Screen Shot 2018-04-14 at 4.12.49 PM

The source code for the finished CAShapeLayer example project is available here:

https://github.com/rtking1993/CAShapeLayers

2 thoughts on “Tutorial: Creating CAShapeLayers using UIBezierPaths

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.