heading

Rounding the corners of a view programmatically has been beautifying our screens since May 1981. In UIKit, the so-called “RoundRect” was made easily achievable for developers in iOS 3 with the addition of the cornerRadius property of any CALayer. But like the old saying goes: there’s more than one way to skin the corners off a cat.

In this post, we’ll take a dive into cornerRadius VS. UIBezierPath.

Method 1: Using the corner radius

1
2
3
4
5
6
7
// Create a view with a magenta background color
let rect = CGRect(x: 0, y: 0, width: 200, height: 200)
let view = UIView(frame: rect)
view.backgroundColor = UIColor.magenta

// Apply a corner radius to the view's layer
view.layer.cornerRadius = 60

Corners - cornerRadius method

But this isn’t the only convenient way that UIKit offers corner rounding. A second option is to create a UIBezierPath of the right shape and apply that as a layer mask to your view. Sounds complicated, but it really isn’t. Here’s how it’s done.

Method 2: Using a UIBezierPath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Create a UIBezierPath
let rect = CGRect(x: 0, y: 0, width: 200, height: 200)
let path = UIBezierPath(roundedRect: rect, cornerRadius: 60)

// Create a mask using the path
let mask = CAShapeLayer()
mask.path = path.cgPath

// Create a view with an orange background color
let view = UIView(frame: rect)
view.backgroundColor = UIColor.orange

// Apply the mask to the subView
view.layer.mask = mask

Corners - bezier method

Comparison of design

Both of these methods round corners, but did you notice that the output is slightly different? Using cornerRadius the rounding is harsh, whereas the UIBezierPath method utilises the changes that apple introduced in iOS 7. If we overlay the views and remove the overlapping parts, we clearly see the difference; the magenta view is less rounded than the orange.

Diff

Now it is evident that the UIBezierPath not only produces a more pleasing curve, but it also matches the style that iOS moved towards from iOS 7. So..

Comparison of development

Although the UIBezierPath method produces a nicer result (IMHO), you need to be considerate about its use. Consider the following very simple example: A UITableViewController with a single prototype cell (with a class of TableViewCell, which has an empty implementation other than an IBOutlet for the view I’m going to round).

Storyboard

We’ll display 1000 cells in a single section; it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
class TableVC: UITableViewController {    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1000
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
    }
}

Implementing the cornerRadius method

There are a number of ways to set the cornerRadius property, the three that come to mind are:

  1. Inside the tableView:cellForRowAt:indexPath method
  2. Inside the tableView:willDisplay:cell method
  3. In the TableViewCell subclass

As this property never needs to change once the cell has been created, placing it in the awakeFromNib method of the TableViewCell subclass works best:

Storyboard

Implementing the UIBezierPath method

If we try setting up the rounding in awakeFromNib for the UIBezierPath method, here is the outcome:

UIBezierPath issues1

Yikes - both initial state and post-rotation states look wrong. You might be tempted to try putting this in the tableView:cellForRowAt:indexPath method to re-draw each time, but that still doesn’t work, and introduces extra performance loss.

UIBezierPath issues2

The problem here is that we’re passing in bounds to create the mask, and these bounds change during loading and rotation. This is an issue because it means that we need to recreate the bounds whenever the layout engine is triggered, which happens a lot - at least once every time a cell is scrolled on screen. Worse still, we have to force a relayout of the cell.contentView before we calculate the new bounds - this is how it looks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TableViewCell: UITableViewCell {

    @IBOutlet weak var roundedView: UIView!

    override func layoutSubviews() {
        super.layoutSubviews()
        
        contentView.setNeedsLayout()
        contentView.layoutIfNeeded()
        
        let path = UIBezierPath(roundedRect: roundedView.bounds, cornerRadius: 60)
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        roundedView.layer.mask = mask
    }
}

Selective cornering with a UIBezierPath

If you can avoid or afford the performance costs, one other nicety with UIBezierPath is that you have control over the rounding at each corner by specifying each UIRectCorner. Here’s how that would look.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Specify the corners that you which to affect
let corners = UIRectCorner.bottomLeft.union(UIRectCorner.topRight)
let radii = CGSize(width: 60, height: 60)

// Create a UIBezierPath
let rect = CGRect(x: 0, y: 0, width: 200, height: 200)
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: radii)

// Create a mask using the path
let mask = CAShapeLayer()
mask.path = path.cgPath

// Create a view with a brown background color
let view = UIView(frame: rect)
view.backgroundColor = UIColor.brown

// Apply the mask to the subView
view.layer.mask = mask

Selective cornering

Conclusion

I love how the UIBezierPath method curves the corners of a view and would love to use it everywhere. However, as it consumes a CGRect which defines its size, it’s important to ensure that you’re defensive against size and orientation changes. This is easy if you know that the size will never change (e.g. App icons on SpringBoard) but not so convenient if the size can be arbitrary - especially if you’re building a library that others may consume in ways for which you haven’t tested.

Further reading

For more information, here are some references to UIKit’s documentation