Native Technologies

SwiftUI: Why are my shadows clipped??

While working on my current project, I encountered a design which made use of an horizontal ++code>ScrollView++/code> containing cards. These cards had an elevation, represented using a small shadow. Nothing unusual, or so it would seem.

When I tried implementing this feature, I started with the card and obtained this result. Nothing wrong. We can even see the shadow as a kind of glow around the card.

And then, I tried to put my shiny card into a ++code>ScrollView++/code>, in the main screen of my app ; and there, we have a problem.

You can see the shadow of the card is cut. It is, in fact, clipped down by the ++code>ScrollView++/code> it is nested in, as you can see on the following picture, in which the ++code>ScrollView++/code> is highlighted.

Why are my shadows clipped??

My answer is not a definitive one, but its my best shot at explaining this behaviour.

++code>UIView++/code> embed a ++code>clipToBounds++/code> property, which, if ++code>true++/code> clips nested views to the bounds of the parent view (as the names cleverly implies). For most subclasses of ++code>UIView++/code>, this property is set to ++code>false++/code> (which is the default value).

But for some, this property is set to true. This is the case for ++code>UIScrollView++/code>, upon which ++code>ScrollView++/code> is built (as well as ++code>UIScrollView++/code> subclasses, such as ++code>UITextView++/code>).

However, the size of ++code>ScrollView++/code> should adapt to its children, right? Why isn't the shadow of my cards taken in account? The answer is blurry, as often when dealing with specifics in the Apple ecosystem / documentation, but here is a layman's explanation of what happens:

  1. the layering engine creates our cards, sets them up in the page and adds the padding everywhere it should
  2. the shadow rendering engine adds the shadow
  3. finally, the clipping engine flattens our components and clips subviews.

The padding is already added when the shadows are rendered, but the clipping is yet to be done.

Yeah, but... Why tho?

This was the technical answer ; not the rationale behind it. As best as I can tell, this is done because, most of the time, this behaviour is what you would expect from a vertical ++code>UIScrollView++/code>, with components being "clipped" as soon as they're scrolled out of the view.

How to fix my shadows?

First solution: do not add shadows to subviews

...But add shadows to the parent view instead.

Because of how view are rendered in SwiftUI, I could apply the ++code>.shadow()++/code> modifier to the ++code>ScrollView++/code> itself. However, this did not make the cut for me, as I needed one card without elevation and this solution would have given every card an elevation, without possibly opting out).

Second solution: add enough padding to let the shadows breathe

If you know the size of your shadows, nothing prevents you from adding an extra padding around the card to push back the bounds of the ++code>ScrollView++/code> and give some space to the shadows.

However, I personally consider this a bad design, as this space is generally the responsibility of the page rather than that of individual components.

Third solution: override the UIKit clipping behaviour

Somewhere in your code, you can add this piece of code:

This will override the ++code>clipToBounds++/code> property, application-wide. This did the trick for me.

If you want to go back to the original behaviour, you can still use the ++code>.clip()++/code> modifier or create a custom ++code>ScrollView++/code> wrapper which would use the ++code>.clip()++/code> modifier.

Another valid solution could be to create a subclass of ++code>UIScrollView++/code> overriding the ++code>.clipToBounds++/code>  property, however this would require to re-implement a whole scroll view component, which is not trivial.

Conclusion

All in all, this small issue was a fun one to work with ; moreover, it highlights some very interesting mechanisms of SwiftUI rendering.

Overall, iOS shadow rendering has many other interesting quirks, but these are stories for later times!

Do you know of any other way solve this particular shadow clipping issue?

Développeur mobile ?

Rejoins nos équipes