Can't add a corner radius and a shadow

21,092

Solution 1

Yes, yes there is...

If you want both a corner radius and a drop shadow, you don't turn on -masksToBounds, but rather set the corner radius and set the bezier path of the shadow with a rounded rect. Keep the radius of the two the same:

[layer setShadowOffset:CGSizeMake(0, 3)];
[layer setShadowOpacity:0.4];
[layer setShadowRadius:3.0f];
[layer setShouldRasterize:YES];

[layer setCornerRadius:12.0f];
[layer setShadowPath:
                   [[UIBezierPath bezierPathWithRoundedRect:[self bounds]
                                               cornerRadius:12.0f] CGPath]];

You might want to check your performance without the -shouldRasterize parameter set once you're setting the shadow path. Drawing performance tends to be very good once you've set a shadow path.

UPDATE

I hadn't looked at this problem in quite awhile, but it appears that you no longer need to set a shadowPath in order to get this to work. Simply setting the cornerRadius and shadowOpacity will work now. I think this has been the case since iOS5 (as far as I can tell). Providing this update is probably unnecessary since setting those parameters 'just works', but I'll provide it for posterity sake. To recap, this is now all you need:

[layer setShadowOpacity:0.4];
[layer setCornerRadius:12.0f];

If you still need better performance, you can go ahead and set the shouldRasterize parameter as well:

[layer setShouldRasterize:YES];

And speaking of performance, it's worth noting that if you are noticing sluggish animations, you will want to use the technique of setting the shadow path after all. This update was really just to point out that setting the path is no longer required to achieve the effect of displaying both a corner radius and a shadow at the same time. If performance is your priority, though, use a path.

UPDATE 2

Since people seem to be having trouble getting this to work in some instances, I'll post a more complete code snippet here from a sample project I created:

- (void)viewDidLoad
{
  [super viewDidLoad];

  CALayer *layer = [CALayer layer];
  [layer setBounds:CGRectMake(0.0f, 0.0f, 100.0f, 200.0f)];
  [layer setPosition:[[self view] center]];
  [layer setBackgroundColor:[[UIColor lightGrayColor] CGColor]];
  [layer setShadowOpacity:0.55f];
  [layer setCornerRadius:8.0f];
  [layer setBorderWidth:1.0f];

  [[[self view] layer] addSublayer:layer];

  [[[self testView] layer] setShadowOpacity:0.55f];
  [[[self testView] layer] setShadowRadius:15.0f];
  [[[self testView] layer] setCornerRadius:8.0f];
  [[[self testView] layer] setBorderWidth:1.0f];
}

The testView is a UIView I added in Interface Builder and set an outlet on. This is to make sure it's working the same on both layers you add explicitly as well as the layers within subviews.

I've tested this on the simulators for iOS5 through iOS6.1. It gives this result for me in each of them:

enter image description here

Solution 2

The following Swift 3 code shows how to draw a shadow and a corner radius on an image by using CAShapeLayer and CALayer.

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // constants
        let radius: CGFloat = 20, offset = 8
        let rect = CGRect(x: 0, y: 0, width: 200, height: 200)

        // roundedView
        let roundedView = UIView()
        view.addSubview(roundedView)

        // shadow layer
        let shadowLayer = CALayer()
        shadowLayer.shadowColor = UIColor.darkGray.cgColor
        shadowLayer.shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: radius).cgPath
        shadowLayer.shadowOffset = CGSize(width: offset, height: offset)
        shadowLayer.shadowOpacity = 0.8
        shadowLayer.shadowRadius = 2
        roundedView.layer.addSublayer(shadowLayer)

        // mask layer
        let maskLayer = CAShapeLayer()
        maskLayer.path = UIBezierPath(roundedRect: rect, cornerRadius: radius).cgPath

        // image layer
        let imageLayer = CALayer()
        imageLayer.mask = maskLayer
        imageLayer.frame = rect
        imageLayer.contentsGravity = kCAGravityResizeAspectFill
        imageLayer.contents = UIImage(named: "image")?.cgImage
        roundedView.layer.addSublayer(imageLayer)

        // auto layout
        roundedView.translatesAutoresizingMaskIntoConstraints = false
        roundedView.widthAnchor.constraint(equalToConstant: rect.width).isActive = true
        roundedView.heightAnchor.constraint(equalToConstant: rect.height).isActive = true
        roundedView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        roundedView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }

}

This code generates the following display:

enter image description here


The previous code can be refactored into the following swift files:

CustomView.swift

import UIKit

class CustomView: UIView {

    var imageLayer: CALayer!
    var image: UIImage? {
        didSet { refreshImage() }
    }

    override var intrinsicContentSize:


        CGSize {
        return CGSize(width: 200, height: 200)
    }

    func refreshImage() {
        if let imageLayer = imageLayer, let image = image {
            imageLayer.contents = image.cgImage
        }
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        if imageLayer == nil {
            let radius: CGFloat = 20, offset: CGFloat = 8

            let shadowLayer = CALayer()
            shadowLayer.shadowColor = UIColor.darkGray.cgColor
            shadowLayer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath
            shadowLayer.shadowOffset = CGSize(width: offset, height: offset)
            shadowLayer.shadowOpacity = 0.8
            shadowLayer.shadowRadius = 2
            layer.addSublayer(shadowLayer)

            let maskLayer = CAShapeLayer()
            maskLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath

            imageLayer = CALayer()
            imageLayer.mask = maskLayer
            imageLayer.frame = bounds
            imageLayer.backgroundColor = UIColor.red.cgColor
            imageLayer.contentsGravity = kCAGravityResizeAspectFill
            layer.addSublayer(imageLayer)
        }


        refreshImage()
    }

}

ViewController.swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let roundedView = CustomView()
        roundedView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(roundedView)

        // auto layout
        let horizontalConstraint = roundedView.centerXAnchor.constraint(equalTo: view.centerXAnchor)
        let verticalConstraint = roundedView.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        NSLayoutConstraint.activate([horizontalConstraint, verticalConstraint])

        roundedView.image = UIImage(named: "image")
    }

}

You can find more ways to combine images with rounded corners and shadow on this Github repo.

Solution 3

Because I use a UIButton with a background image none of these answers worked for me. I keep getting either no shadow or no round edges on my buttons.

The easiest way in my scenario was to just add another view behind the button and add the shadow to it like so:

button.clipsToBounds=YES;
button.layer.cornerRadius = 25;

UIView *shadowView = [[UIView alloc]initWithFrame:button.frame];

shadowView.backgroundColor = [UIColor whiteColor];//needs this to cast shadow
shadowView.layer.cornerRadius = 25;
shadowView.clipsToBounds = YES;
shadowView.layer.masksToBounds = NO;
shadowView.layer.shadowOffset = CGSizeMake(0, 2);
shadowView.layer.shadowRadius = 1;
shadowView.layer.shadowOpacity = 0.2;


[[button superview]addSubview:shadowView];
[[button superview]bringSubviewToFront:button];
Share:
21,092
Denis
Author by

Denis

Updated on August 09, 2020

Comments

  • Denis
    Denis almost 4 years

    I'm trying to draw a shadow and a corner radius on an image. I can add them separately, but I've got no way to add both effects at the same time. I'm adding a shadow with:

    [layer setShadowOffset:CGSizeMake(0, 3)];
    [layer setShadowOpacity:0.4];
    [layer setShadowRadius:3.0f];
    [layer setShouldRasterize:YES];
    

    Here, layer is a CALayer of a UIView subclass. So this works whenever I set

    [layer setMasksToBounds:NO];
    

    Now to add a corner radius I do this:

    [layer setCornerRadius:7.0f];
    

    but I need to set MasksToBounds to YES in order for this to work:

    [layer setMasksToBounds:YES];
    

    Is there anyway I can get both of these effects to add?

    Thanks for your time,

    Denis

  • memmons
    memmons over 13 years
    Is there any reason not to set shouldRasterize to YES?
  • Matt Long
    Matt Long over 13 years
    It depends on how often you're updating the layer. If you intend to set it once and not update it again, rasterizing it will help performance if you're animating the layer. However, if the layer's content gets updated often, it will render that content as an image every time the content changes which could actually hurt your scrolling performance (assuming you're scrolling/animating).
  • infinity26
    infinity26 over 13 years
    Needs a space between UIBezierPath and bezierPathWithRoundedRect.
  • Matt Long
    Matt Long over 13 years
    Thanks Ari. I fixed it in the answer.
  • taber
    taber over 12 years
    Maybe something changed in iOS 5, but I can't seem to get this working in awakeFromNib on a UIImageView for some reason... I need to set masksToBounds to NO for the shadow to appear.
  • Wayne Liu
    Wayne Liu almost 12 years
    I noticed that if we use this technique to a UIImageView and assume that the image content fills the whole frame, then the image would "bleed" at the 4 corners. Setting masksToBounds would fix the bleeding, but obviously the shadow disappears. Pity.
  • Nicolas Manzini
    Nicolas Manzini over 11 years
    Great answer Bezier path rocks
  • Paul Batum
    Paul Batum over 11 years
    I tried this and for me, the shadow now has a corner radius but the image does not. I can't find a way for corner radius to work without setting maskToBounds to YES (which kills the shadow).
  • Matt Long
    Matt Long over 11 years
    @PaulBatum Another trick I've used is to use multiple layers. Use masksToBounds:YES on your image layer, and then create a layer behind the image layer that provide the shadow.
  • nielsbot
    nielsbot over 11 years
    Pretty sure using the shadowPath option is faster, since it doesn't involve drawing your layer and then creating a shadow based on that image.
  • Matt Long
    Matt Long over 11 years
    I think you're right, @nielsbot, however, the question is about displaying both a corner radius and a drop shadow at the same time. I was simply wanting to note that it's no longer necessary to set a bezier path to achieve that. I'll add that clarification to the update. Thanks.
  • GangstaGraham
    GangstaGraham almost 11 years
    This did not work for me (although it seems to have worked for other people), is there something different with iOS6? The corners are not rounded here, but the shadows exist. I would like both. Thanks @MattLong
  • Matt Long
    Matt Long almost 11 years
    @JoshSmith I'm not sure what the problem might be. I've added another update to my answer that will hopefully help you get it working. Let me know.
  • Thomás Pereira
    Thomás Pereira almost 7 years
    Look for Swift 3 & IBInspectable extension solution: stackoverflow.com/a/43958505/3052059
  • alones
    alones over 4 years
    If you put an image on a view that has corner and shadow, this link is useful. fluffy.es/rounded-corner-shadow A container view has both properties, and the put an image view with corner on the container view.