Multiple shadows on UIView
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 elevation
generic 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.border
seems 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 applyShadows
over 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 valueperimeterShadow
. 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 theduration
andtimingFunction
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.