Better shadow performance on views

There are two different uses for the shadowPath property on CALayer:

  1. Improving the performance of having a shadow
  2. Creating shadows that don’t match the contents of the view. Check out Apple’s Using Shadow Path for Special Effects.

For performance reasons, always set a shadowPath. This is a substantial improvement, especially if the view changes position via animation or presence in a scroll view.

When you can set a path

The shadowPath tells the system what should be casting a shadow without having to look at the contents of the view itself. Since most views that need a shadow are opaque, we just need to describe the appearance of the background of the view.

Using the convenience initializers on UIBezierPath we can create ovals, squares and rounded rectangles without difficulty. For more complicated paths, check out A Primer on Bézier Curves. You can still use UIBezierPath or CGPath to create them, but it will require more complicated math.

Starting with a simple, purple view with a shadow:

let purpleView = UIView()
purpleView.backgroundColor = .purple
purpleView.layer.shadowRadius = 10.0
purpleView.layer.shadowColor = UIColor.black.cgColor
purpleView.layer.shadowOffset = CGSize()
purpleView.layer.shadowOpacity = 0.8

We can tell the system to draw a shadow for the entire square:

purpleView.layer.shadowPath = UIBezierPath(rect: purpleView.bounds).cgPath

For rounded corners, we can set the cornerRadius property on the layer, and create a matching shadowPath:

purpleView.layer.cornerRadius = 16.0
purpleView.layer.shadowPath = UIBezierPath(roundedRect: view.bounds, cornerRadius: 16.0).cgPath

When you can’t set a path

Sometimes it’s not possible to set a path because there’s no easy way to describe the contents of the view. For example, text is a mess of random contents. Rasterizing the layer avoids having to draw the shadow repeatedly.

// create our label
let label = UILabel()
label.textColor = .purple
label.text = NSLocalizedString("Swift Lemma!", comment: "")
label.layer.shadowOpacity = 0.6
label.layer.shadowColor = UIColor.black.cgColor
label.layer.shadowOffset = CGSize(width: 0, height: 2)

// render and cache the layer
label.layer.shouldRasterize = true
// make sure the cache is retina (the default is 1.0)
label.layer.rasterizationScale = UIScreen.main.scale

This produces a view that looks like this:

"Swift Lemma!" with a shadow

Keep in mind

Always set the shadowPath inside either layoutSubviews() or viewDidLayoutSubviews(). Since Auto Layout likely means there aren’t constant sizes for views, setting a shadowPath elsewhere may become outdated or incorrect.

When creating a path, the coordinate system for the path is the layer it’s applied to. To make it easier, pretend the shadow path is a subview. For this reason, we use the bounds of the view to create its shadow path.