What's the best way to add a drop shadow to my UIView
Asked Answered
P

7

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.

Phyte answered 18/3, 2012 at 18:46 Comment(0)
M
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
}
Mercia answered 18/3, 2012 at 19:6 Comment(8)
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 > youtu.be/tdpemc_XdpsPhyte
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?Beutler
@JohnErck try this: UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:5.0];. Not tested but should yield the result you want.Mercia
I have learned that when animating the view, the shadow leaves gray tracks while moving. Removing the shadowPath lines solved thisGeehan
why does this have to be done in layoutSubviews()? -- i'm finding that this code only works when executed inside itStavro
@Stavro 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.Mercia
@Mercia but my view's size doesn't change, only it's positionStavro
@Stavro 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)Mercia
D
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)
Dwarf answered 12/12, 2014 at 13:45 Comment(5)
or even better in viewDidLayoutSubviews()Detrain
prefer swift struct initialisers over Make variants, i.e. CGSize(width: 0, height: 0.5)Dermatologist
I add this code to layoutSubviews(), it still didnt render in IB. Are there other settings i have to opt in?Ajar
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!Beanie
@Bart va Kuil Thank you for layoutSubviews hint!Loosen
M
13

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

view.layer.masksToBounds = NO;

and it should work.

(Source)

Mages answered 18/3, 2012 at 19:7 Comment(0)
M
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
        }
    }
}
Marita answered 6/4, 2017 at 15:41 Comment(4)
how to refer to that extension in interface builderHuda
IBInspectable makes it available in the interface builderHoshi
Can I achieve this in Objective C too ?Matte
if you want to have a real-time preview (in interface builder), here you can find a good article about it spin.atomicobject.com/2017/07/18/swift-interface-builderHuckaby
U
8

You can set shadow to your view from storyboard also

enter image description here

Usually answered 22/3, 2018 at 7:12 Comment(0)
S
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)
Sympathizer answered 13/4, 2016 at 7:12 Comment(4)
Just enforce targetView: UIView and remove the bangs.Vertumnus
Why not use self? Seems much more convenient to me.Halfhearted
A better way to do this is to remove targetView as a parameter and use self instead of targetView. This eliminates redundancy and allows one to call it like so: sampleView.addDropShadowToView()Pentagram
Max Nelson's comment are great. My suggestion is use if let syntax instead of !Dispirit
M
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

Mayemayeda answered 3/5, 2019 at 23:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.