Multiple shadows on UIView

Rajdeep Kwatra
5 min readMar 20, 2019

--

Recently, I came across a requirement of having multiple shadows on a single UIView. The requirement was pretty straightforward:

  • Have a perimeter shadow with1px width.
  • Have an ambient shadow which will be variable to give an impression of elevation, as in Material design.

The expected result was as follows:

I wanted to make this elevationgeneric such that it can be applied to any view. My experience on this piece of code turned out to be rather eventful unlike what I was expecting. In this article, I am going to talk about the evolution of the code that I used for getting what our designer was looking for.

At first glance, it seemed like I can just use layer.border for the perimeter shadow, however it did not yield the result as what was expected. Using layer.borderseems much darker and has no spread like what you get if you use a shadow instead:

To keep the approach generic, I created an extension called applyShadowsover UIView and added the code as shown in snippets below.

1st attempt — Using BezierPaths

The first attempt was to use UIBezierPath on the layer of view:

And similarly, ambientShadow can also be created with the only difference being in shadowRadius and shadowOffset to give an impression of elevation. The following code then adds the shadow layers to the view’s layer:

While this worked, it seemed that using sublayers would simplify the code a little.

2nd attempt — Using Sublayers

When using a sublayer we can avoid the additional code required to define the shadowPath:

This code looks much cleaner and straightforward than using UIBezierPath and result is exactly the same output.

It all seemed to be working fine until I tried it on the view the elevation was expected to be applied. As shown in the graphic below, the elevation code did not account for rounding of subviews of the view on which elevation was being applied. This resulted in a broken UI:

As visible in the graphic above, while the parent view has rounded corners and shadows, the subview is not rounded.

3rd attempt — Insets on shadow layer

To fix the issue of rounding, I could not have used clipsToBound as it would result in shadows being clipped as well. As a workaround, I thought of using edge insets equal to the cornerRadius.

Using insets for the frame of the shadow seemed to fix the issue:

However, this approach can be used in some specific cases only. For e.g. this approach will not work if the subview is scrollable and is expected to be touching the parent view bounds while scrolling. Following graphic shows the issue visually:

In the graphic above, the yellow color is of the parent view and cyan is the subview. This approach was more of a hack than a solution. This hides the issue rather than fixing it.

4th attempt — Masking subviews

UIView provides a property called mask which seemed to be the solution to the problem I encountered in the 3rd attempt. Using a mask , we can round the subview without having the hack of insets on shadows:

With the masks applied, we get the expected result:

With this attempt, we solved the problem of non-rounding of subviews and we do not have any additional insets either. While it seems that this approach work, this again poses issues with scrollable subviews:

In the gif above, the blue color is of the parent view which contains a scrollable child view. The child view has 9 items but we see only 4 as others are hidden by the mask we applied.

5th attempt — Using additional view for shadows

So the solution to the problem that I kept running into again and again was clear — I’ll have to use clipToBounds on the view to get desired rounding at the corners. This also meant that I can no longer have shadows applied on the view in question. However, I can still achieve the desired appearance using the following approach:

  • Create an additional view with same frame as the current.
  • Add shadows using sublayers approach to the new view.
  • Position the new view behind the current view.
  • Set clipsToBounds on the current view.

and that’s it.

I then updated the code to add shadows to the shadowView:

With this change in place, we are able to get shadows applied to a view with rounded corners and a scrollable subview:

Bonus tips

  • Layer names: you must have noticed that I have set layer.name to a value perimeterShadow. This serves as an identifier to remove/resize layers where I can just query all the sublayers to get the shadow layers.
  • Background color for layers: If you don’t set the layer.backgroundColor, the shadows will not show up.
  • Layer animation: You can query layer’s animation for key, position to get the duration and timingFunction of the view. This can then be used to resize the shadows with the view as the view size changes as a result of change in orientation.

--

--

Rajdeep Kwatra

Rajdeep is an iOS developer at Atlassian. He believes that a piece of code can always be improved, but the cost may not always be justified against the benefits