Search code examples
iosswiftuibezierpath

How to add a shadow to a view that's masked with a path


How do I add a shadow to a view that's masked with a path?

layer has a mask property that you can set like this yourCustomView.layer.mask = somePath. But how do I add a shadow that's also masked to the layer.mask?


Solution

  • I see this asked quite a bit and answers are all over the place, so I thought I would centralize a tad.

    Basically when you want to add a shadow to a view that's masked with a path, things get interesting.

    This is an extension I built that handles it all and is really easy to use. The corners argument is setup for a basic rounded corners setup, but you can replace that with whatever UIBezierPath you want.

    import UIKit
    
    public extension UIView {
        /// You will probably need to call this from viewDidLayoutSubviews()
        func roundCorners(corners: UIRectCorner = .bottomRight, radius: CGFloat = 40) {
            layoutIfNeeded()
    
            let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
            let mask = CAShapeLayer()
            mask.path = path.cgPath
            self.layer.mask = mask
        }
    
        /// You will probably need to call this from viewDidLayoutSubviews()
        func roundCornersWithShadow(corners: UIRectCorner = .bottomRight, cornerRadius radius: Double = 40, shadowOpacity: Float = 0.5, shadowRadius: Double = 4, shadowOffset: CGSize = CGSize(width: 0, height: 2), shadowColor: UIColor = .black, maskName: String = "mask", shadowName: String = "shadow") {
    
            layoutIfNeeded()
    
            let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)).cgPath
    
            let maskLayer = CAShapeLayer()
            maskLayer.frame = bounds
            maskLayer.path = path
            layer.mask = maskLayer
            layer.name = maskName
    
            if let subLayers = superview?.layer.sublayers {
                for layer in subLayers where layer.name == shadowName { layer.removeFromSuperlayer() }
            }
    
            let shadowLayer = CAShapeLayer()
            shadowLayer.name = shadowName
            shadowLayer.path = path
            shadowLayer.frame = frame
    
            shadowLayer.shadowOpacity = shadowOpacity
            shadowLayer.shadowRadius = CGFloat(shadowRadius)
            shadowLayer.shadowColor = shadowColor.cgColor
            shadowLayer.shadowOffset = shadowOffset
    
            superview?.layer.insertSublayer(shadowLayer, below: layer)
        }
    }
    

    Then use it something like:

    override func viewDidLayoutSubviews() {
        ...
        yourView.roundCornersWithShadow(corners: [.bottomRight, .bottomLeft, .topRight, .topLeft]) // Use whatever corners you want.
    }