'UIBezierPath Shadow with transparent internal rect Objective c

I have a problem by adding shadow using UIbezierPath. My Code is

CGRect f = view.bounds;
view.layer.shadowColor = [UIColor redColor].CGColor;
view.layer.shadowOpacity = 1;
view.layer.shadowRadius = 10;
CGFloat shadowWidth = 5;
CGRect shadowRect = CGRectMake(-shadowWidth, -shadowWidth, f.size.width+(shadowWidth*2), f.size.height+(shadowWidth*2));
CGFloat shadowRadius = radius;
view.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:shadowRect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(shadowRadius, shadowRadius)].CGPath;
view.layer.shadowOffset = CGSizeMake(0, 0);

I am trying to add red shadow using this code. The problem is I am setting transparent background color of my view. Due to this the added red shadow layer becomes visible on background, rather the parent background color. like following image

Problem in setting shadow

But I want it to be like this

Expected output

If there is any solution for the problem please guide.



Solution 1:[1]

You can do this by

  • adding a CAShapeLayer as a sublayer
  • give it a rounded-rect path
  • give the path a White fill color
  • then use a "mask with a rectangle cut out of the center"

Here's a quick example view subclass and a controller demonstrating it:

Custom UIView subclass

class ShadowPathView: UIView {
    
    let radius: CGFloat = 10
    
    let shadowLayer = CAShapeLayer()
    let maskLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        // these properties don't change
        backgroundColor = .clear
        
        layer.addSublayer(shadowLayer)
        
        shadowLayer.fillColor = UIColor.white.cgColor
        shadowLayer.shadowColor = UIColor.red.cgColor
        shadowLayer.shadowOpacity = 1.0
        shadowLayer.shadowOffset = .zero
        
        // set the layer mask
        shadowLayer.mask = maskLayer
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        shadowLayer.frame = bounds
        shadowLayer.path = UIBezierPath(roundedRect: bounds, cornerRadius: radius).cgPath
        
        // create a rect bezier path, large enough to exceed the shadow bounds
        let bez = UIBezierPath(rect: bounds.insetBy(dx: -radius * 2.0, dy: -radius * 2.0))
        
        // create a path for the "hole" in the layer
        let holePath = UIBezierPath(rect: bounds.insetBy(dx: radius, dy: radius))
        
        // this "cuts a hole" in the path
        bez.append(holePath)
        bez.usesEvenOddFillRule = true
        maskLayer.fillRule = .evenOdd
        
        // set the path of the mask layer
        maskLayer.path = bez.cgPath
        
        let w: CGFloat = 5
        // make the shadow rect larger than bounds
        let shadowRect = bounds.insetBy(dx: -w, dy: -w)
        // set the shadow path
        //  make the corner radius larger to make the curves look correct
        shadowLayer.shadowPath = UIBezierPath(roundedRect: shadowRect, cornerRadius: radius + w).cgPath
        
    }
    
}

Example view controller

class ShadowPathVC: UIViewController {
    
    // two of our custom ShadowPathView
    let v1 = ShadowPathView()
    let v2 = ShadowPathView()
    
    // a label to put UNDER the second view
    let underLabel = UILabel()
    
    // a label to add as a SUVBVIEW of the second view
    let subLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = UIColor(red: 0.8, green: 0.92, blue: 0.97, alpha: 1.0)
        
        [underLabel, subLabel].forEach { v in
            v.textAlignment = .center
            v.backgroundColor = .green
        }
        [v1, v2, underLabel, subLabel].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
        }
        [v1, underLabel, v2].forEach { v in
            view.addSubview(v)
        }
        v2.addSubview(subLabel)
        underLabel.text = "This label is Under the shadow view"
        subLabel.text = "This label is a subview of the shadow view"
        subLabel.numberOfLines = 0

        let g = view.safeAreaLayoutGuide
        
        NSLayoutConstraint.activate([
            
            v1.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
            v1.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            v1.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            v1.heightAnchor.constraint(equalToConstant: 120.0),

            v2.topAnchor.constraint(equalTo: v1.bottomAnchor, constant: 80.0),
            v2.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
            v2.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
            v2.heightAnchor.constraint(equalToConstant: 160.0),
            
            underLabel.leadingAnchor.constraint(equalTo: v2.leadingAnchor, constant: -20.0),
            underLabel.topAnchor.constraint(equalTo: v2.topAnchor, constant: -20.0),
            underLabel.heightAnchor.constraint(equalToConstant: 80.0),
            
            subLabel.bottomAnchor.constraint(equalTo: v2.bottomAnchor, constant: -12.0),
            subLabel.trailingAnchor.constraint(equalTo: v2.trailingAnchor, constant: -40.0),
            subLabel.widthAnchor.constraint(equalToConstant: 120.0),
            
        ])
    }
}

How it looks:

enter image description here


Edit - I should have caught the need for Objective-C implementation (fewer and fewer requests for that theses days).

So, here's the same as above, but in Obj-C (default headers):

Custom UIView subclass

#import "ShadowPathView.h"

@interface ShadowPathView ()
{
    CAShapeLayer *shadowLayer;
    CAShapeLayer *maskLayer;
    CGFloat radius;
}
@end

@implementation ShadowPathView

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self commonInit];
    }
    return self;
}

- (void) commonInit {
    radius = 10;
    shadowLayer = [CAShapeLayer new];
    maskLayer = [CAShapeLayer new];
    
    self.backgroundColor = [UIColor clearColor];
    
    [self.layer addSublayer:shadowLayer];
    
    shadowLayer.fillColor = [UIColor whiteColor].CGColor;
    shadowLayer.shadowColor = [UIColor redColor].CGColor;
    shadowLayer.shadowOpacity = 1.0;
    shadowLayer.shadowOffset = CGSizeZero;
    
    // set the layer mask
    shadowLayer.mask = maskLayer;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    
    shadowLayer.frame = self.bounds;
    shadowLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:radius].CGPath;
    
    // create a rect bezier path, large enough to exceed the shadow bounds
    UIBezierPath *bez = [UIBezierPath bezierPathWithRect:CGRectInset(self.bounds, -radius, -radius)];
    
    // create a path for the "hole" in the layer
    UIBezierPath *holePath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, 0, 0) cornerRadius:radius];

    // this "cuts a hole" in the path
    [bez appendPath:holePath];
    bez.usesEvenOddFillRule = YES;
    maskLayer.fillRule = kCAFillRuleEvenOdd;
    
    // set the path of the mask layer
    maskLayer.path = bez.CGPath;
    
    CGFloat shadowWidth = 5;
    // make the shadow rect larger than bounds
    CGRect shadowRect =  CGRectInset(self.bounds, -shadowWidth, -shadowWidth);
    // set the shadow path
    //  make the corner radius larger to make the curves look correct
    shadowLayer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:shadowRect cornerRadius:radius + shadowWidth].CGPath;
}

@end

Example view controller

#import "ViewController.h"
#import "ShadowPathView.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor colorWithRed:0.8 green:0.92 blue:0.97 alpha:1.0];
    
    // two of our custom ShadowPathView
    ShadowPathView *v1 = [ShadowPathView new];
    ShadowPathView *v2 = [ShadowPathView new];

    // a label to put UNDER the second view
    UILabel *underLabel = [UILabel new];

    // a label to add as a SUVBVIEW of the second view
    UILabel *subLabel = [UILabel new];

    for (UILabel *v in @[underLabel, subLabel]) {
        v.textAlignment = NSTextAlignmentCenter;
        v.backgroundColor = [UIColor greenColor];
    }
    for (UIView *v in @[v1, v2, underLabel, subLabel]) {
        v.translatesAutoresizingMaskIntoConstraints = NO;
    }
    for (UIView *v in @[v1, underLabel, v2]) {
        [self.view addSubview:v];
    }
    [v2 addSubview:subLabel];
    underLabel.text = @"This label is Under the shadow view";
    subLabel.text = @"This label is a subview of the shadow view";
    subLabel.numberOfLines = 0;

    UILayoutGuide *g = self.view.safeAreaLayoutGuide;
    
    [NSLayoutConstraint activateConstraints:@[
        
        [v1.topAnchor constraintEqualToAnchor:g.topAnchor constant:40.0],
        [v1.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:40.0],
        [v1.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-40.0],
        [v1.heightAnchor constraintEqualToConstant:120.0],
        
        [v2.topAnchor constraintEqualToAnchor:v1.bottomAnchor constant:80.0],
        [v2.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:40.0],
        [v2.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-40.0],
        [v2.heightAnchor constraintEqualToConstant:160.0],
        
        [underLabel.leadingAnchor constraintEqualToAnchor:v2.leadingAnchor constant:-20.0],
        [underLabel.topAnchor constraintEqualToAnchor:v2.topAnchor constant:-20.0],
        [underLabel.heightAnchor constraintEqualToConstant:80.0],
        
        [subLabel.bottomAnchor constraintEqualToAnchor:v2.bottomAnchor constant:-12.0],
        [subLabel.trailingAnchor constraintEqualToAnchor:v2.trailingAnchor constant:-40.0],
        [subLabel.widthAnchor constraintEqualToConstant:120.0],
        
    ]];
    
}


@end

Solution 2:[2]

Thanks @DonMag! I got help from his class ShadowPathView to edit my Objective C function as follows

- (void)setOnView:(UIView *)view {
    CGFloat radius = 10;
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    CAShapeLayer *shadowLayer = [CAShapeLayer layer];
    [view.layer addSublayer:shadowLayer];
    shadowLayer.shadowColor = [UIColor redColor].CGColor;
    shadowLayer.shadowOpacity = 1;
    shadowLayer.shadowOffset = CGSizeMake(0, 0);
    shadowLayer.mask = maskLayer;
    shadowLayer.frame = view.bounds;
    shadowLayer.path = [UIBezierPath bezierPathWithRoundedRect:view.bounds cornerRadius:radius].CGPath;
    UIBezierPath *bez = [UIBezierPath bezierPathWithRect:CGRectInset(view.bounds, -radius, -radius)];
    UIBezierPath *holePath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(view.bounds, 0, 0) cornerRadius:radius];
    [bez appendPath:holePath];
    bez.usesEvenOddFillRule = YES;
    maskLayer.fillRule = kCAFillRuleEvenOdd;
    maskLayer.path = bez.CGPath;
    CGFloat shadowWidth = 5;
    CGRect shadowRect =  CGRectInset(view.bounds, -shadowWidth, -shadowWidth);
    shadowLayer.shadowPath = [UIBezierPath bezierPathWithRoundedRect:shadowRect cornerRadius:radius].CGPath;
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 GNChishti