109

I am trying to add a drop shadow to views that are layered on top of one another, the views collapse allowing content in other views to be seen, in this vein i want to keep view.clipsToBounds ON so that when the views collapse their content is clipped.

This seems to have made it difficult for me to add a drop shadow to the layers as when i turn clipsToBounds ON the shadows are clipped also.

I have been trying to manipulate view.frame and view.bounds in order to add a drop shadow to the frame but allow the bounds to be large enough to encompass it, however I have had no luck with this.

Here is the code I am using to add a Shadow (this only works with clipsToBounds OFF as shown)

view.clipsToBounds = NO;
view.layer.shadowColor = [[UIColor blackColor] CGColor];
view.layer.shadowOffset = CGSizeMake(0,5);
view.layer.shadowOpacity = 0.5;

Here is a screenshot of the shadow being applied to the top lightest grey layer. Hopefully this gives an idea of how my content will overlap if clipsToBounds is OFF.

Shadow Application.

How can I add a shadow to my UIView and keep my content clipped?

Edit: Just wanted to add that I have also played around with using background images with shadows on, which does work well, however I would still like to know the best coded solution for this.

Cœur
  • 37,241
  • 25
  • 195
  • 267
Wez
  • 10,555
  • 5
  • 49
  • 63

7 Answers7

281

Try this:

UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:view.bounds];
view.layer.masksToBounds = NO;
view.layer.shadowColor = [UIColor blackColor].CGColor;
view.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
view.layer.shadowOpacity = 0.5f;
view.layer.shadowPath = shadowPath.CGPath;

First of all: The UIBezierPath used as shadowPath is crucial. If you don't use it, you might not notice a difference at first, but the keen eye will observe a certain lag occurring during events like rotating the device and/or similar. It's an important performance tweak.

Regarding your issue specifically: The important line is view.layer.masksToBounds = NO. It disables the clipping of the view's layer's sublayers that extend further than the view's bounds.

For those wondering what the difference between masksToBounds (on the layer) and the view's own clipToBounds property is: There isn't really any. Toggling one will have an effect on the other. Just a different level of abstraction.


Swift 2.2:

override func layoutSubviews()
{
    super.layoutSubviews()

    let shadowPath = UIBezierPath(rect: bounds)
    layer.masksToBounds = false
    layer.shadowColor = UIColor.blackColor().CGColor
    layer.shadowOffset = CGSizeMake(0.0, 5.0)
    layer.shadowOpacity = 0.5
    layer.shadowPath = shadowPath.CGPath
}

Swift 3:

override func layoutSubviews()
{
    super.layoutSubviews()

    let shadowPath = UIBezierPath(rect: bounds)
    layer.masksToBounds = false
    layer.shadowColor = UIColor.black.cgColor
    layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
    layer.shadowOpacity = 0.5
    layer.shadowPath = shadowPath.cgPath
}
Candost
  • 1,029
  • 1
  • 12
  • 28
pkluz
  • 4,871
  • 4
  • 26
  • 40
  • 1
    Thanks, I tried your code and then tried adding `masksToBounds = NO;` to my original - with both attempts i kept `clipsToBounds = YES;` ON - both failed to clip the content. heres a screencap of what happend with your example > http://youtu.be/tdpemc_Xdps – Wez Mar 18 '12 at 19:55
  • 1
    Any ideas on how to get the drop shadow to hug a rounded corner rather than rendering as if the corner is still a square? – John Erck Mar 26 '14 at 16:07
  • 8
    @JohnErck try this: `UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:5.0];`. Not tested but should yield the result you want. – pkluz Mar 26 '14 at 20:14
  • 1
    I have learned that when animating the view, the shadow leaves gray tracks while moving. Removing the shadowPath lines solved this – ishahak Sep 06 '15 at 02:07
  • why does this have to be done in `layoutSubviews()`? -- i'm finding that this code only works when executed inside it – shoe Jul 20 '17 at 13:17
  • 1
    @shoe because any time size of the view changes, `layoutSubviews()` is called. And if we don't compensate in that place by adjusting the `shadowPath` to reflect the current size, we'd have a resized view but the shadow would still be the old (initial) size and either not show or peek out where it shouldn't. – pkluz Jul 20 '17 at 14:24
  • @pkluz but my view's size doesn't change, only it's position – shoe Jul 20 '17 at 14:27
  • @shoe `layoutSubviews()` is still called. Also remember that views when they are initialized don't immediately start with the right size. So if you were to apply the `shadowPath` right after `init`, you're not guaranteed to have the right sizing yet. `layoutSubviews()` guarantees that you can respond to any change. And that includes responding to position changes too. (Think about directional shadows... - you could implement this too) – pkluz Jul 20 '17 at 15:14
66

Wasabii's answer in Swift 2.3:

let shadowPath = UIBezierPath(rect: view.bounds)
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.blackColor().CGColor
view.layer.shadowOffset = CGSize(width: 0, height: 0.5)
view.layer.shadowOpacity = 0.2
view.layer.shadowPath = shadowPath.CGPath

And in Swift 3/4/5:

let shadowPath = UIBezierPath(rect: view.bounds)
view.layer.masksToBounds = false
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 0.5)
view.layer.shadowOpacity = 0.2
view.layer.shadowPath = shadowPath.cgPath

Put this code in layoutSubviews() if you're using AutoLayout.

In SwiftUI, this is all much easier:

Color.yellow  // or whatever your view
    .shadow(radius: 3)
    .frame(width: 200, height: 100)
Bart van Kuik
  • 4,704
  • 1
  • 33
  • 57
  • 11
    Thanks for noting `layoutSubviews` – Islam Aug 25 '15 at 12:32
  • 1
    or even better in viewDidLayoutSubviews() – Max MacLeod Nov 11 '15 at 13:14
  • 1
    prefer swift struct initialisers over Make variants, i.e. `CGSize(width: 0, height: 0.5)` – Oliver Atkinson Nov 23 '15 at 15:29
  • I add this code to layoutSubviews(), it still didnt render in IB. Are there other settings i have to opt in? – donkey Nov 26 '15 at 15:15
  • Thanks. I was using Auto Layout and I thought to my self - well.. `view.bounds` has not been created so obviously that `shadowPath` can't reference the view's rect but some `CGRectZero` instead. Anyway, putting this under `layoutSubviews` solved my problem at last! – devdc Mar 16 '16 at 04:29
  • @Bart va Kuil Thank you for `layoutSubviews ` hint! – imike Jan 31 '19 at 15:13
13

You can create an extension for UIView to access these values in the design editor

Shadow options in design editor

extension UIView{

    @IBInspectable var shadowOffset: CGSize{
        get{
            return self.layer.shadowOffset
        }
        set{
            self.layer.shadowOffset = newValue
        }
    }

    @IBInspectable var shadowColor: UIColor{
        get{
            return UIColor(cgColor: self.layer.shadowColor!)
        }
        set{
            self.layer.shadowColor = newValue.cgColor
        }
    }

    @IBInspectable var shadowRadius: CGFloat{
        get{
            return self.layer.shadowRadius
        }
        set{
            self.layer.shadowRadius = newValue
        }
    }

    @IBInspectable var shadowOpacity: Float{
        get{
            return self.layer.shadowOpacity
        }
        set{
            self.layer.shadowOpacity = newValue
        }
    }
}
Chris Stillwell
  • 10,266
  • 10
  • 67
  • 77
13

The trick is defining the masksToBounds property of your view's layer properly:

view.layer.masksToBounds = NO;

and it should work.

(Source)

Hemang
  • 26,840
  • 19
  • 119
  • 186
sergio
  • 68,819
  • 11
  • 102
  • 123
8

You can set shadow to your view from storyboard also

enter image description here

pallavi
  • 513
  • 7
  • 13
3

On viewWillLayoutSubviews:

override func viewWillLayoutSubviews() {
    sampleView.layer.masksToBounds =  false
    sampleView.layer.shadowColor = UIColor.darkGrayColor().CGColor;
    sampleView.layer.shadowOffset = CGSizeMake(2.0, 2.0)
    sampleView.layer.shadowOpacity = 1.0
}

Using Extension of UIView:

extension UIView {

    func addDropShadowToView(targetView:UIView? ){
        targetView!.layer.masksToBounds =  false
        targetView!.layer.shadowColor = UIColor.darkGrayColor().CGColor;
        targetView!.layer.shadowOffset = CGSizeMake(2.0, 2.0)
        targetView!.layer.shadowOpacity = 1.0
    }
}

Usage:

sampleView.addDropShadowToView(sampleView)
Alvin George
  • 14,148
  • 92
  • 64
0

So yes, you should prefer the shadowPath property for performance, but also: From the header file of CALayer.shadowPath

Specifying the path explicitly using this property will usually * improve rendering performance, as will sharing the same path * reference across multiple layers

A lesser known trick is sharing the same reference across multiple layers. Of course they have to use the same shape, but this is common with table/collection view cells.

I don't know why it gets faster if you share instances, i'm guessing it caches the rendering of the shadow and can reuse it for other instances in the view. I wonder if this is even faster with

Rufus Mall
  • 569
  • 4
  • 14