Search code examples
iosswiftxcodecore-graphicscalayer

Shadows masks to bounds only on the left and upper side. What causes this?


I'm trying to achieve a feature that allows adding a shadow to a transparent button. For this, I'm creating a layer that masks the shadows inside the view. However, my shadows are clipped on the left and upper sides but not clipped on the right and lower sides.

Here is how it looks (This is not a transparent button but they're also working fine except the shadow being clipped like this.):

enter image description here

And here is my code for achieving this:

private func applyShadow() {
    layer.masksToBounds = false

    if shouldApplyShadow && shadowLayer == nil {
        shadowLayer = CAShapeLayer()

        let shapePath = CGPath(roundedRect: bounds, cornerWidth: cornerRadi, cornerHeight: cornerRadi, transform: nil)

        shadowLayer.path = shapePath
        shadowLayer.fillColor = backgroundColor?.cgColor
        shadowLayer.shadowPath = shadowLayer.path
        shadowLayer.shadowRadius = shadowRadius ?? 8
        shadowLayer.shadowColor = (shadowColor ?? .black).cgColor
        shadowLayer.shadowOffset = shadowOffset ?? CGSize(width: 0, height: 0)
        shadowLayer.shadowOpacity = shadowOpacity ?? 0.8

        layer.insertSublayer(shadowLayer!, at: 0)

        /// If there's background color, there is no need to mask inner shadows.
        if backgroundColor != .none && !(innerShadows ?? false) {
            let maskLayer = CAShapeLayer()
            maskLayer.path = { () -> UIBezierPath in
                let path = UIBezierPath()
                path.append(UIBezierPath(cgPath: shapePath))
                path.append(UIBezierPath(rect: UIScreen.main.bounds))
                path.usesEvenOddFillRule = true
                return path
            }().cgPath

            maskLayer.fillRule = .evenOdd
            shadowLayer.mask = maskLayer
        }
    }
}

I think that's something related to the Even-Odd Fill Rule algorithm, I'm not sure. But how can I overcome this clipping problem?

Thanks in advance.

EDIT: This is what a transparent button with borders and text on it looks when shadow applied.. Which I don't want.

enter image description here

What it should look like this. No shadows inside but also a clear background color. (Except the clipped top and left sides):

enter image description here


Solution

  • I think a couple issues...

    1. You are appending UIBezierPath(rect: UIScreen.main.bounds) to your path, but that puts the top-left corner at the top-left corner of the layer... which "clips" the top-left shadow.

    2. If you DO have a background color, you'll need to clip those corners as well, or they will "bleed" outside the rounded corners.

    Give this a try (only slightly modified). It's designated @IBDesignable so you can see how it looks in Storyboard / Interface Builder (I did not set any of the properties to inspectable -- I'll leave that up to you if you want to do so):

    @IBDesignable
    class MyRSButton: UIButton {
    
        var shouldApplyShadow: Bool = true
        var innerShadows: Bool?
    
        var cornerRadi: CGFloat = 8.0
    
        var shadowLayer: CAShapeLayer!
        var shadowRadius: CGFloat?
        var shadowColor: UIColor?
        var shadowOffset: CGSize?
        var shadowOpacity: Float?
    
        override func layoutSubviews() {
            super.layoutSubviews()
            applyShadow()
        }
    
        private func applyShadow() {
    
            // needed to prevent background color from bleeding past
            // the rounded corners
            cornerRadi = bounds.size.height * 0.5
            layer.cornerRadius = cornerRadi
    
            layer.masksToBounds = false
    
            if shouldApplyShadow && shadowLayer == nil {
                shadowLayer = CAShapeLayer()
    
                let shapePath = CGPath(roundedRect: bounds, cornerWidth: cornerRadi, cornerHeight: cornerRadi, transform: nil)
    
                shadowLayer.path = shapePath
                shadowLayer.fillColor = backgroundColor?.cgColor
                shadowLayer.shadowPath = shadowLayer.path
                shadowLayer.shadowRadius = shadowRadius ?? 8
                shadowLayer.shadowColor = (shadowColor ?? .black).cgColor
                shadowLayer.shadowOffset = shadowOffset ?? CGSize(width: 0, height: 0)
                shadowLayer.shadowOpacity = shadowOpacity ?? 0.8
    
                layer.insertSublayer(shadowLayer!, at: 0)
    
                /// If there's background color, there is no need to mask inner shadows.
                if backgroundColor != .none && !(innerShadows ?? false) {
                    let maskLayer = CAShapeLayer()
                    maskLayer.path = { () -> UIBezierPath in
                        let path = UIBezierPath()
                        path.append(UIBezierPath(cgPath: shapePath))
    
                        // define a rect that is 80-pts wider and taller
                        // than the button... this will "expand" it from center
                        let r = bounds.insetBy(dx: -40, dy: -40)
    
                        path.append(UIBezierPath(rect: r))
    
                        path.usesEvenOddFillRule = true
                        return path
                        }().cgPath
    
                    maskLayer.fillRule = .evenOdd
                    shadowLayer.mask = maskLayer
                }
            }
    
        }
    
    }
    

    Result:

    enter image description here