Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add a shadow to a custom UIView created with BezierPath?

I have created a TicketView using Bezier Paths, which looks like this:

Ticket View

Now I am trying to add a shadow to it, but I encountered a problem. I also have an image on the left side of this view, and the rounded corners of this ticket view is supposed to work as a mask to it. However, when I apply the mask, the shadow does not work as expected. So far, the only workaround I have found is to add a container view behind the TicketView and apply the shadow to it. Unfortunately, this approach does not make the shadow follow the custom path I created for the TicketView.

Is there any way to apply a shadow that will correctly follow the custom path I created, while still keeping the mask applied?

Here is the code for reference:

import UIKit

/// A card view with rounded corners on the left side and a zig-zag shape on the right side
class TicketView: UIView {
    /// The radius used for the left side of this view
    let radius: CGFloat
    /// The zig-zag's width
    let zigZagsWidth: CGFloat
    /// The position within the card's view where the zig-zag starts (1.0 = the end of the view)
    let zigZagStartPosition: CGFloat

    public init(
        radius: CGFloat = 10,
        zigZagsWidth: CGFloat = 5,
        zigZagStartPosition: CGFloat = 1.0,
        frame: CGRect = .zero
    ) {
        self.radius = radius
        self.zigZagsWidth = zigZagsWidth
        self.zigZagStartPosition = zigZagStartPosition
        super.init(frame: frame)
        self.backgroundColor = .clear
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.5
        layer.shadowRadius = 5.0
        layer.shadowOffset = CGSize(width: 0, height: 0)
        layer.shadowPath = (layer.mask as? CAShapeLayer)?.path
    }

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

        guard let context = UIGraphicsGetCurrentContext() else {
            return
        }

        let path = UIBezierPath()

        // Top-Left
        let topLeftCorner = CGPoint(x: bounds.minX, y: bounds.minY)
        let bottomLeftCorner = CGPoint(x: bounds.minX, y: bounds.maxY)

        path.move(to: CGPoint(x: topLeftCorner.x, y: topLeftCorner.y))
        path.addArc(withCenter: CGPoint(x: topLeftCorner.x + radius, y: topLeftCorner.y + radius),
                    radius: radius,
                    startAngle: .pi,
                    endAngle: .pi * 1.5,
                    clockwise: true)

        // Zig-Zag
        let zigzagHeight: CGFloat = rect.size.height / (rect.size.height / 7.3)
        let zigzagWidth: CGFloat = zigZagsWidth
        let numberOfSegments = Int(rect.size.height / zigzagHeight) + 1

        for index in 0..<numberOfSegments {
            let inY = CGFloat(index) * zigzagHeight
            let inX = (index % 2 == 0) ? rect.size.width * zigZagStartPosition :
            rect.size.width * zigZagStartPosition - zigzagWidth
            path.addLine(to: CGPoint(x: inX, y: inY))

            let zigZagCurveRadius: CGFloat = 2
            let zigZagCurveCenter = CGPoint(
                x: inX + zigZagCurveRadius,
                y: inY + zigZagCurveRadius
            )
            path.addArc(withCenter: zigZagCurveCenter,
                        radius: zigZagCurveRadius,
                        startAngle: .pi,
                        endAngle: .pi,
                        clockwise: true)
        }

        // Bottom-Left
        path.addLine(to: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y))
        path.addArc(withCenter: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y - radius),
                    radius: radius,
                    startAngle: .pi * 1.5,
                    endAngle: .pi,
                    clockwise: true)

        path.close()

        context.setFillColor(UIColor.white.cgColor)
        path.fill()

        // Mask
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        layer.mask = mask
    }
}
like image 272
chr0x Avatar asked Sep 18 '25 21:09

chr0x


1 Answers

The problem is that the layer you are masking is the same layer that you apply the shadow to. This causes the shadow to be masked, i.e. not visible.

Adding a container view works and applying the shadow to the container view works, because now the mask is applied to the view in the container, and the shadow is on the container, so the shadow is not masked away.

You can also add a subview inside TicketView - the shadow is applied to TicketView, but the mask is applied to the subview.

Or, just add a sublayer. The shadow is applied to TicketView.layer, but the mask is applied to the sublayer.

I'm not sure how you observed "this approach does not make the shadow follow the custom path I created for the TicketView". If you have set shadowPath to the correct path, then it would work. That said, the actual path of the shadows on zig zags like this can be hard to see. To see that this does work, exaggerate the zigzag width, and reduce the shadow radius. You should see that the shadow path follows the zigzag.

enter image description here

I've used the "apply shadow to TicketView, and apply mask to a subview of TicketView" approach to create that. Full code (things I've changed are addressed in the comments):

class TicketView: UIView {
    let radius: CGFloat
    let zigZagsWidth: CGFloat
    let zigZagStartPosition: CGFloat
    
    // the subview that we are applying a mask to
    let maskedSubview: UIView

    public init(
        radius: CGFloat = 10,
        zigZagsWidth: CGFloat = 50, // exaggerated this
        zigZagStartPosition: CGFloat = 1.0,
        frame: CGRect = .zero
    ) {
        self.radius = radius
        self.zigZagsWidth = zigZagsWidth
        self.zigZagStartPosition = zigZagStartPosition
        
        // create the subview in init
        maskedSubview = UIView()
        maskedSubview.backgroundColor = .white
        
        super.init(frame: frame)
        
        addSubview(maskedSubview)
        maskedSubview.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            maskedSubview.topAnchor.constraint(equalTo: topAnchor),
            maskedSubview.bottomAnchor.constraint(equalTo: bottomAnchor),
            maskedSubview.leadingAnchor.constraint(equalTo: leadingAnchor),
            maskedSubview.trailingAnchor.constraint(equalTo: trailingAnchor),
        ])
        
        // set shadow properties except shadowPath here
        layer.shadowColor = UIColor.black.cgColor
        layer.shadowOpacity = 0.5
        layer.shadowRadius = 1 // made this smaller to see more clearly
        layer.shadowOffset = CGSize(width: 0, height: 0)
        self.backgroundColor = .clear
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        
        // layoutSubviews is called, so the view bounds might have changed
        // recompute the mask path
        let mask = CAShapeLayer()
        mask.path = makeTicketPath().cgPath

        // apply mask to maskedSubview.layer
        maskedSubview.layer.mask = mask

        // apply shadow to self.layer
        layer.shadowPath = mask.path
    }
    
    // I've extracted the path-drawing function:
    private func makeTicketPath() -> UIBezierPath {
        let path = UIBezierPath()
        let rect = bounds
        let topLeftCorner = CGPoint(x: bounds.minX, y: bounds.minY)
        let bottomLeftCorner = CGPoint(x: bounds.minX, y: bounds.maxY)

        path.move(to: CGPoint(x: topLeftCorner.x, y: topLeftCorner.y))
        path.addArc(withCenter: CGPoint(x: topLeftCorner.x + radius, y: topLeftCorner.y + radius),
                    radius: radius,
                    startAngle: .pi,
                    endAngle: .pi * 1.5,
                    clockwise: true)

        let zigzagHeight: CGFloat = rect.size.height / (rect.size.height / 7.3)
        let zigzagWidth: CGFloat = zigZagsWidth
        let numberOfSegments = Int(rect.size.height / zigzagHeight) + 1

        for index in 0..<numberOfSegments {
            let inY = CGFloat(index) * zigzagHeight
            let inX = (index % 2 == 0) ? rect.size.width * zigZagStartPosition :
            rect.size.width * zigZagStartPosition - zigzagWidth
            path.addLine(to: CGPoint(x: inX, y: inY))

            let zigZagCurveRadius: CGFloat = 2
            let zigZagCurveCenter = CGPoint(
                x: inX + zigZagCurveRadius,
                y: inY + zigZagCurveRadius
            )
            path.addArc(withCenter: zigZagCurveCenter,
                        radius: zigZagCurveRadius,
                        startAngle: .pi,
                        endAngle: .pi,
                        clockwise: true)
        }

        path.addLine(to: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y))
        path.addArc(withCenter: CGPoint(x: bottomLeftCorner.x + radius, y: bottomLeftCorner.y - radius),
                    radius: radius,
                    startAngle: .pi * 1.5,
                    endAngle: .pi,
                    clockwise: true)

        path.close()
        return path
    }
}
like image 177
Sweeper Avatar answered Sep 20 '25 11:09

Sweeper