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

Viewed 210

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

1 Answers

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

Related