Views with Rounded Corners and Shadows

Tim Johnson
Swifty Tim
Published in
5 min readSep 13, 2017

For the last few years the overarching design pattern in apps, both mobile and web, has been flat. With iOS 7 and Material Design both Google and Apple (following in the footsteps of Windows 8, let’s be real here) announced flashy new design patterns.

Flat UI is pretty awesome. It removed the funky looking 3D UI elements that definitely felt a little too 2000's.

With the announce of iOS 11, though, Apple seems to be moving away from the more prominent flat only UI, to once again incorporating drop shadows and other depth related effects.

Using shadows to create depth

It is still a very flat look, just with some additional information for the user indicating how to interact with the UI.

Something of note in the above image is that the cell has both rounded corners, as well as a drop shadow.

In order to round corners on a CALayer, you need to do something like the following.

extension CALayer {
func roundCorners(radius: CGFloat) {
self.cornerRadius = radius
}
}

This applies a corner radius to the layer — only applying the corner radius to the background color and the border. This is important, because if the layer has any contents then masksToBounds needs to be set to true in order for the corner radius to clip the content.

In order to add a shadow to a CALayer, just a few more lines of code.

extension CALayer {
func addShadow() {
self.shadowOffset = .zero
self.shadowOpacity = 0.2
self.shadowRadius = 10
self.shadowColor = UIColor.black.cgColor
self.masksToBounds = false
}
}

Having both a shadow and rounded corners on a view isn’t a problem until the layer backing the view has contents (this could range from an image to other non trivial view elements). As stated above, if a layer’s contents exists, then masksToBounds needs to be set to true for the rounded corners to apply. Clipping the content and displaying the shadow are two states that cannot exist simultaneously.

And that makes sense. If you want a shadow on a view, you can’t have a mask on the view that clips it to the bounds, because a shadow displays outside of a views bounds. To clip content with rounded corners, you need to do the opposite.

So adding a shadow and rounding the corners of a view isn’t quite as trivial as it sounds.

There are a few easy-to-implement solutions to this. One solution is to use a container view to encapsulate subviews with rounded corners, while the underlying container view has a shadow. This is great, until you want to have shadows and rounded corners on more than one or two views in your application.

It would be really great to have some sort of helper method that doesn’t affect the view hierarchy. Of course, building an extension that mimics the view hierarchy as it stands is possible, but it can be tedious. Luckily, it’s much easier to manipulate things at the view layer.

CALayer has a handy property contents, which represents all the contents in a layer. This value is often nil, but in a UIImageView it is the image, and can be non-nil in other cases.

A layer’s contents can be transferred to another layer, and the underlying content won’t change. This is really great, because this is what we want to do — move the content of the layer so that we can apply a corner radius, and keep a shadow on the original layer.

if let contents = self.contents {
self.contents = nil
let contentLayer = CALayer()
contentLayer.name = Constants.contentLayerName
contentLayer.contents = contents
contentLayer.frame = bounds
contentLayer.cornerRadius = cornerRadius
contentLayer.masksToBounds = true
insertSublayer(contentLayer, at: 0)
}

Here, we are moving the current layer’s contents to the content layer, applying the corner radius, clipping the content to the bounds, and inserting the content layer as the first sublayer. This is a very similar approach to the UIView approach described above, but it doesn’t manipulate the view layout.

If we set masksToBounds to false on the current layer and add a shadow, then we get the desired effect of having both a shadow and a corner radius.

Unfortunately if there are any sublayers that fill the bounds of the parent layer, then those sublayers aren’t going to be clipped accordingly.

In order to achieve this effect, we need to manually enforce the corner radius.

sublayers?.filter{ $0.frame.equalTo(self.bounds) }
.forEach{ $0.roundCorners(radius: self.cornerRadius) }

This sets the desired corner radius on any sublayer in the current layer hierarchy.

Overall, this has the desired effect of rounding the corners on the UIView and displaying a shadow. The following function combines the two previous methods.

private func addShadowWithRoundedCorners() {
if let contents = self.contents {
masksToBounds = false
sublayers?.filter{ $0.frame.equalTo(self.bounds) }
.forEach{ $0.roundCorners(radius: self.cornerRadius) }
self.contents = nil if let sublayer = sublayers?.first,
sublayer.name == Constants.contentLayerName {

sublayer.removeFromSuperlayer()
}
let contentLayer = CALayer()
contentLayer.name = Constants.contentLayerName
contentLayer.contents = contents
contentLayer.frame = bounds
contentLayer.cornerRadius = cornerRadius
contentLayer.masksToBounds = true
insertSublayer(contentLayer, at: 0)
}
}

There is some additional logic here — mostly to account for not adding redundant content layers.

We assign a specified name to the content layer once it is added. On subsequent calls to this method, perhaps if it is being called in layoutSubviews, then we check to see if a content layer already exists. If it does, it is removed. A new content layer with the current contents is then added.

Nothing actually happens if the layer doesn’t have any contents. In this case, simply applying a corner radius will work.

Now, in our roundCorners and addShadow functions, we can call addShadowWithRoundedCorners, and our layers will apply the appropriate changes as needed.

extension CALayer {
func addShadow() {
self.shadowOffset = .zero
self.shadowOpacity = 0.2
self.shadowRadius = 10
self.shadowColor = UIColor.black.cgColor
self.masksToBounds = false
if cornerRadius != 0 {
addShadowWithRoundedCorners()
}
}
func roundCorners(radius: CGFloat) {
self.cornerRadius = radius
if shadowOpacity != 0 {
addShadowWithRoundedCorners()
}
}
}

This is a nice and clean way to abstract out the logic that enables shadows and rounded corners on any UIView or CALayer, and it can be extended beyond just rounded corners to any kind of mask.

I hope you enjoyed this post, and feel free to leave any feedback!

--

--