On the importance of setting shadowPath

It’s super easy to add drop shadows to any view in iOS. All you need to do is

  1. add QuartzCore framework to your project (if not there already)
  2. import QuartzCore into your implementation file
  3. add a line such as [myView.layer setShadowOpacity:0.5]

and voilà, your view now has a drop shadow.

However, the easy way is rarely the best way in terms of performance.  If you have to animate this view (and especially if it’s part of a UITableViewCell) you will probably notice stutters in the animation.  This is because calculating the drop shadow for your view requires Core Animation to do an offscreen rendering pass to determine the exact shape of your view in order to figure out how to render its drop shadow.  (Remember, your view could be any complex shape, possibly even with holes in it.)

To convince yourself of this, turn on the Color Offscreen-Rendered option in the Simulator’s Debug menu.

Alternately, target a physical device, launch Instruments (⌘I), choose the Core Animation template, select the Core Animation instrument, and check the Color Offscreen-Rendered Yellow option.

Then in the Simulator (or on your device) you will see something like this:

Which indicates that something (in our case the drop shadow) is forcing an expensive offscreen rendering pass.

The quick fix

Fortunately, fixing the drop shadow performance is typically almost as easy as adding a drop shadow.  All you need to do is provide Core Animation with some information about the shape of your view to help it along.  Calling setShadowPath: on your view’s layer does exactly that:

[myView.layer setShadowPath:[[UIBezierPath 
    bezierPathWithRect:myView.bounds] CGPath]];

(Note: your code will vary depending on the actual shape of your view.  UIBezierPath has many convenience methods, including bezierPathWithRoundedRect:cornerRadius: in case you’ve rounded the corners of your view.)

Now run it again and confirm that the yellow wash for offscreen-rendered content is gone.

The catch

You will need to update the layer’s shadowPath each time the bounds of your view change.  And if you’re animating a change to bounds, then you will also need to animate the change to the layer’s shadowPath to match.  This will need to be a CAAnimation because UIView cannot animate shadowPath (which is a property on CALayer).  Fortunately, it is straight-forward to animate from one CGPath to another (from the old to new shadowPath) via CAKeyframeAnimation.