Hello! Welcome back to our final lesson in the "UI & Rendering Performance Optimization" module.
In our previous lesson, we did a deep dive into the performance costs of custom drawing. We established that overriding draw(rect:) is a powerful tool, but it comes with significant overhead: it's CPU-bound, executes on the main thread, and requires a memory-intensive backing store. This can be a major source of UI stutter, especially during scrolling or animations.
Today, we'll learn the modern, high-performance solution to this problem. This lesson will show you how to replace a draw(rect:) implementation with a CAShapeLayer to improve rendering performance. You will learn not just the "how" but also the "why," understanding the architectural difference that makes CAShapeLayer so much more efficient.
Why CAShapeLayer is the Right Tool for the Job
Before we write any code, let's solidify why we are making this change. The core difference lies in which processor does the work.
As we discussed, draw(rect:) forces the CPU to create a bitmap, which is then sent to the GPU. A CAShapeLayer, on the other hand, is a special subclass of CALayer designed to draw vector shapes defined by a CGPath. The layer is given a description of the shape (the path), and the GPU handles the rasterization directly.
This GPU-centric approach has several key advantages:
- Performance: The GPU is highly optimized for this kind of work, freeing up the CPU on the main thread to handle user interaction and business logic.
- Memory Efficiency:
CAShapeLayerdoesn't need the intermediate memory buffer (the backing store) that adraw(rect:)view requires. The GPU renders the path directly. - Animatability: Properties of the
CAShapeLayer, such as itspath,strokeColor, orlineWidth, can be animated smoothly by Core Animation on the GPU. Trying to animate a shape drawn withdraw(rect:)would require callingsetNeedsDisplay()on every frame, re-running the CPU-bound drawing code repeatedly.
The following article provides an excellent introduction to CAShapeLayer and its performance benefits over draw(rect:).
A Practical Guide to CAShapeLayer
The article 'A Practical Guide to CAShapeLayer' by Dimas Ashidiqi starts by perfectly summarizing the performance distinction between draw(_:) and CAShapeLayer.
Please read the 'Introduction to CAShapeLayer' section. Focus on the distinction it makes between CPU and GPU usage.
Now that we're clear on the "why," let's get practical and perform the refactoring.
Refactoring a draw(rect:) View
Imagine you have a simple custom view that draws a bordered circle. A common way to implement this would be using draw(rect:).
The "Old" Way: draw(rect:)
// This is a CPU-bound implementation.
class CircleView: UIView {
// Ensure the view has a clear background so we see our drawing.
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
}
required init?(coder: NSCoder) {
super.init(coder: coder)
backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
// Create a circular path that fits within the view's bounds.
let path = UIBezierPath(ovalIn: rect.insetBy(dx: 2, dy: 2))
// Configure the drawing context.
path.lineWidth = 4
UIColor.systemRed.setStroke()
// Execute the drawing command on the CPU.
path.stroke()
}
}
This code works, but as we know, it runs on the CPU and creates a backing store. During our profiling in previous lessons, this is the kind of implementation that would cause the Time Profiler to show significant time spent in -[UIView drawRect:].

The "New" Way: CAShapeLayer
Let's refactor CircleView to use CAShapeLayer. The process involves four main steps, which are clearly laid out in the guide we just read. We will apply them to our CircleView.
A Practical Guide to CAShapeLayer
Let's continue with 'A Practical Guide to CAShapeLayer'. This section provides the exact blueprint for our refactoring.
Read the 'Getting Started' section. Pay close attention to the four steps outlined for creating and configuring the CAShapeLayer and how the properties like fillColor, strokeColor, and lineWidth are set on the layer itself, not through a drawing context.
Following those steps, our new implementation looks like this:
// This is a GPU-accelerated implementation.
class ShapeLayerCircleView: UIView {
// 1. Create the CAShapeLayer instance as a private property.
private let circleLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupLayer()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupLayer()
}
private func setupLayer() {
// 3. Customize the layer's properties.
// Note: We use .cgColor because CALayer works with Core Graphics types.
circleLayer.strokeColor = UIColor.systemRed.cgColor
circleLayer.lineWidth = 4
// Ensure the inside of the circle is transparent.
circleLayer.fillColor = UIColor.clear.cgColor
// 4. Add the CAShapeLayer to the view's layer as a sublayer.
layer.addSublayer(circleLayer)
}
// This method is called whenever the view's bounds change.
override func layoutSubviews() {
super.layoutSubviews()
// 2. Create the path and assign it to the layer.
// We do this here to ensure the path is updated if the view is resized.
let circularPath = UIBezierPath(ovalIn: bounds.insetBy(dx: 2, dy: 2))
circleLayer.path = circularPath.cgPath
// Ensure the layer's frame matches the view's bounds.
circleLayer.frame = bounds
}
}
Let's break down the crucial differences:
- No
draw(rect:): The most important change is that we have completely removed theoverride func draw(_ rect: CGRect). The view no longer does any custom CPU drawing. - Declarative Properties: Instead of imperative drawing commands like
setStroke()andstroke(), we declaratively set properties on thecircleLayer(e.g.,strokeColor,lineWidth). This tells Core Animation what to draw, and it figures out how to do it efficiently on the GPU. layoutSubviews():draw(rect:)is automatically called by the system when the view's geometry changes. Since we've removed it, we need a new place to update our circle's path when the view resizes.layoutSubviews()is the correct place for this. It's called whenever the view'sboundsorframechanges, making it the perfect trigger to recalculate the path. This is a critical detail for creating robust, resizable components.CGColor:CALayerproperties expectCGColorinstead ofUIColor. You can always get theCGColorfrom aUIColorby accessing its.cgColorproperty.

By making this change, we have successfully offloaded the drawing work from the CPU to the GPU, eliminated a memory-hungry backing store, and created a component that is not only more performant but also ready to be animated efficiently.
Conclusion
This lesson concludes our module on UI and Rendering Performance. You've learned to identify and fix some of the most common and impactful rendering bottlenecks in iOS development.
Key Takeaways:
CAShapeLayeris a high-performance, GPU-accelerated alternative to CPU-bounddraw(rect:)for drawing custom vector shapes.- Refactoring involves creating a
CAShapeLayer, defining its shape via thepathproperty, styling it with properties likestrokeColorandfillColor, and adding it as a sublayer to your view. - Unlike
draw(rect:), which is called automatically, you must manually update aCAShapeLayer's path when the view's geometry changes. ThelayoutSubviews()method is the standard place to perform this update. - By preferring
CAShapeLayeroverdraw(rect:)for custom shapes, you write code that is more memory-efficient, performs better, and is easier to animate.
Preview of the Next Module:
We've now covered performance from the perspectives of memory management, CPU usage, and GPU rendering. In our next module, "Network Performance Optimization," we'll turn our attention to another critical area. Our first lesson will equip you with the foundational skill to profile network activity and inspect requests/responses using the Network instrument. This will provide the visibility you need to start diagnosing and solving network-related bottlenecks in your apps.