Search code examples
iosswiftuikitcalayeruibezierpath

Adding Corner radius for selected edges with shadow using UIBezierPath


I am trying to add a shadow on my view. It is having 3 rounded edges. To achieve this UIBezierPath and then setting the CAShapelayer with this path as the mask on the layer of the view. Now, if I'm trying to add shadow on this view, it is not showing. I have gone through a similar question and suggested answers but nothing works in my case. Following is my implementation:

class BubbleView: UIView {

override func draw(_ rect: CGRect) {
    super.draw(rect)
}

override func layoutSubviews() {
    super.layoutSubviews()
    self.updateContainerLayer()
}

func updateContainerLayer() {
    let brazierPath: UIBezierPath = UIBezierPath(roundedRect: self.bounds,
                                                 byRoundingCorners: [.bottomRight, .bottomLeft, .topLeft],
                                                 cornerRadii: CGSize(width: 15.0, height: 0.0))

    //1
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = brazierPath.cgPath
    self.layer.mask = shapeLayer

    //2
    self.layer.shadowColor  = UIColor(r: 0, g: 0, b: 0, alpha: 0.25).cgColor
    self.layer.shadowOpacity = 1.0
    self.layer.shadowOffset = CGSize(width: 0.0, height: 0.5)
    self.layer.shadowRadius = 1.5
    self.layer.shadowPath   = brazierPath.cgPath
    //3
    self.layer.masksToBounds = true
    self.clipsToBounds = false
    //4
    self.layer.shouldRasterize = true
    self.layer.rasterizationScale = UIScreen.main.scale


}

}


Solution

  • If you are targeting iOS 11+ you can use the layer's .maskedCorners property:

    class BubbleView: UIView {
    
        // don't override draw()
    //  override func draw(_ rect: CGRect) {
    //      super.draw(rect)
    //  }
    
        override func layoutSubviews() {
            super.layoutSubviews()
            self.updateContainerLayer()
        }
    
        func updateContainerLayer() {
            let brazierPath: UIBezierPath = UIBezierPath(roundedRect: self.bounds,
                                                         byRoundingCorners: [.bottomRight, .bottomLeft, .topLeft],
                                                         cornerRadii: CGSize(width: 15.0, height: 0.0))
    
            //1
    //      let shapeLayer = CAShapeLayer()
    //      shapeLayer.path = brazierPath.cgPath
    //      self.layer.mask = shapeLayer
    
            // iOS 11+ use .maskedCorners
            self.layer.cornerRadius = 15.0
            self.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
    
            //2
            self.layer.shadowColor  = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
            self.layer.shadowOpacity = 1.0
            self.layer.shadowOffset = CGSize(width: 0.0, height: 0.5)
            self.layer.shadowRadius = 1.5
            self.layer.shadowPath   = brazierPath.cgPath
            //3
            self.layer.masksToBounds = true
            self.clipsToBounds = false
            //4
            self.layer.shouldRasterize = true
            self.layer.rasterizationScale = UIScreen.main.scale
    
    
        }
    }
    

    Result:

    enter image description here

    Result with an exaggerated .shadowOffset = CGSize(width: -10.0, height: 10.5) to make it easy to see the shadow:

    enter image description here

    If you need to allow earlier iOS versions, I believe you'll need to use a container view approach.


    EDIT:

    Another approach, using a "container" view for the shadow. This will work with iOS earlier than 11... it uses the same UIBezierPath for the "content view" mask and the shadow path:

    class BubbleView: UIView {

    let contentView: UIView = {
        let v = UIView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    func commonInit() -> Void {
    
        addSubview(contentView)
    
        NSLayoutConstraint.activate([
            contentView.topAnchor.constraint(equalTo: topAnchor),
            contentView.bottomAnchor.constraint(equalTo: bottomAnchor),
            contentView.leadingAnchor.constraint(equalTo: leadingAnchor),
            contentView.trailingAnchor.constraint(equalTo: trailingAnchor),
            ])
    
        self.clipsToBounds = false
    
        backgroundColor = .clear
        contentView.backgroundColor = .red
    
        // set non-changing properties here
        contentView.layer.masksToBounds = true
    
        self.layer.shadowColor  = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
        self.layer.shadowOpacity = 1.0
    
        self.layer.shadowOffset = CGSize(width: 0.0, height: 0.5)
    
        // exaggerated shadow offset so we can see it easily
        //self.layer.shadowOffset = CGSize(width: -10.0, height: 10.5)
    
        self.layer.shadowRadius = 1.5
    
        self.layer.shouldRasterize = true
        self.layer.rasterizationScale = UIScreen.main.scale
    
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
    
        let bezierPath = UIBezierPath(roundedRect: self.bounds,
                                      byRoundingCorners: [.bottomRight, .bottomLeft, .topLeft],
                                      cornerRadii: CGSize(width: 15.0, height: 0.0))
    
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = bezierPath.cgPath
        contentView.layer.mask = shapeLayer
    
        self.layer.shadowPath   = bezierPath.cgPath
    
    }
    

    }

    As with the 11+ example, Result:

    enter image description here

    and Result with an exaggerated .shadowOffset = CGSize(width: -10.0, height: 10.5) to make it easy to see the shadow:

    enter image description here