Skip to content
cyberbarker.xyz

Rounded corners and a shadow on UIKit cells

In UIKit you can't simply round a view's corners and give the same view a shadow — getting both takes a hack. In a collection view it gets worse: every cell, header and footer needs its own shadow setup and its own offset.

Put the shadow on the collection view once, and keep the rounded corners on a background view inside each reusable view. This is still a correct way to work with UIKit, and it takes about a minute.

Why one view can't have both

A card usually has content that must follow its rounded corners: an image, a colored header, a separator. For that the card needs clipsToBounds = true, and clipping also removes the shadow. Turn clipping off and the shadow comes back, but the content no longer respects the corners:

Cards with a shadow and no clipping: the colored strip at the top has square corners

Shadow on the card, no clipping: the indigo strip sticks out of the rounded corners.

The usual way out is a shadow set up in every reusable view, with its own offset. That is the routine this trick removes.

The usual per-view shadow

Shadow code on each reusable view usually looks like this:

yourView.layer.shadowColor = UIColor.red.cgColor
yourView.layer.shadowOpacity = 0.25
yourView.layer.shadowOffset = CGSize(width: 1, height: 1)
yourView.layer.shadowRadius = 2

The same lines repeat in every cell, header and footer, each with its own offset.

Clear backgrounds, one rounded background view

Set the background of the collection view and of every reusable view to .clear.

The only exception is the views in the storyboard that act as the visible background. That can be any UIView subclass with any settings, cornerRadius included. These views can keep clipsToBounds = true: the shadow no longer lives on them.

Move the shadow to the collection view

Take the shadow setup out of layoutSubviews() of the reusable view and put it into viewWillLayoutSubviews() or viewDidLayoutSubviews() of the view controller. Use the collection view as yourView:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    collectionView.layer.shadowColor = UIColor.red.cgColor
    collectionView.layer.shadowOpacity = 0.25
    collectionView.layer.shadowOffset = CGSize(width: 1, height: 1)
    collectionView.layer.shadowRadius = 2
}

That's it:

Rounded cards with a shadow that follows the rounded corners

Cards clipped to their rounded corners, cells and collection view clear, one shadow on the collection view.

Why it works

The shadow of the collection view's layer takes the shape of what is visible inside it — all the cells together. With clear backgrounds, the only visible things are the rounded background views, so the one shadow gets their rounded shape. The cards stay clipped, and the shadow is set up in one place instead of in every reusable view.

Where it breaks

Everything between the collection view and the rounded view has to stay clear. If a cell keeps its own background color, the shadow follows the cell's rectangle instead of the card:

Cards inside cells with a white background: square white corners and a rectangular shadow

The same setup with backgroundColor = .white on the cell: square corners and a rectangular shadow.

The fix: final version

Make every layer between the collection view and the card clear: the cell, its contentView and the collection view. The card keeps its color, its corners and its clipping. The shadow is set once, on the collection view.

final class CardCell: UICollectionViewCell {
    let card = UIView()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .clear
        contentView.backgroundColor = .clear

        card.backgroundColor = .white
        card.layer.cornerRadius = 16
        card.clipsToBounds = true
        card.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(card)
        NSLayoutConstraint.activate([
            card.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            card.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            card.topAnchor.constraint(equalTo: contentView.topAnchor),
            card.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
        ])
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
}
final class CardsViewController: UIViewController {
    var collectionView: UICollectionView!

    override func viewDidLoad() {
        super.viewDidLoad()
        // … create the collection view and register CardCell
        collectionView.backgroundColor = .clear
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        collectionView.layer.shadowColor = UIColor.black.cgColor
        collectionView.layer.shadowOpacity = 0.35
        collectionView.layer.shadowOffset = CGSize(width: 0, height: 4)
        collectionView.layer.shadowRadius = 8
    }
}

With the cell background back to .clear, the same cards get rounded corners and a shadow that follows them:

Fixed: rounded cards with a shadow that follows the rounded corners

After the fix: cell and contentView clear, card clipped, one shadow on the collection view.