Search code examples
iosshadowtransparentuibezierpath

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

  • 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