Search code examples
iosswiftuiviewcore-graphics

Shadow set on uibezierpath in draw(rect:) removed when view is redrawn


Problem:

I've implemented a collapsible header view with a curved bottom border and a gradient layer that sits above my scrollView. The header has a drop shadow that is drawn in its draw(rect:) function. The shadow gives the bottom border a glow effect. Everything looks good, but when setNeedsDisplay() is called on my header or the view is redrawn, the drop shadow goes away.

Question:

How do I make the drop shadow persist when the view is redrawn?

Code:

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

    let p1 = CGPoint(x: 0, y: self.frame.height * 0.8)
    let p2 = CGPoint(x: self.frame.width / 2, y: self.frame.height * 1.06)
    let p3 = CGPoint(x: self.frame.width, y: self.frame.height * 0.8)
    let p4 = CGPoint(x: self.frame.width, y: 0)
    let p5 = CGPoint(x: 0, y: 0)

    let path = UIBezierPath()

    path.move(to: p1)
    path.addQuadCurve(to: p3, controlPoint: p2)
    path.addLine(to: p4)
    path.addLine(to: p5)
    path.addLine(to: p1)
    path.close()

    self.shapeLayer.path = path.cgPath
    self.layer.mask = self.shapeLayer

    if !self.didSetGradient {

        self.gradient = CustomColors.blueGreenBackgroundGradient(frame: path.bounds)

        self.gradient.mask = shapeLayer
        self.gradient.masksToBounds = true
        self.gradient.removeFromSuperlayer()

        self.layer.insertSublayer(self.gradient, at: 0)

        self.didSetGradient = true
    }

    let context = UIGraphicsGetCurrentContext()

    context?.saveGState()
    context?.setShadow(offset: CGSize(width: 0, height: 2.5), blur: 15, color: CustomColors.green.cgColor)
    CustomColors.blueGreen.setStroke()
    UIColor.clear.setFill()
    path.lineWidth = 5
    path.stroke()
    context?.restoreGState()        
}

Solution

  • Aight was able to figure this one out. The bezier path shadow was getting hidden when the view was redrawn because the layer mask was getting set to the shapeLayer. My solution was to draw the curved shadow using the layer's shadowPath property and inserting the shapeLayer as a subLayer instead of masking the layer. Make sure to add the shapeLayer before you add the gradient, so the gradient appears on top of the shapeLayer. The didSetGradient block is pretty tacky as @dfd mentioned, but it's working for now!

    override func draw(_ rect: CGRect) {
        super.draw(rect)
    
        let p1 = CGPoint(x: 0, y: self.frame.height * 0.8)
        let p2 = CGPoint(x: self.frame.width / 2, y: self.frame.height * 1.06)
        let p3 = CGPoint(x: self.frame.width, y: self.frame.height * 0.8)
        let p4 = CGPoint(x: self.frame.width, y: 0)
        let p5 = CGPoint(x: 0, y: 0)
    
        let path = UIBezierPath()
    
        path.move(to: p1)
        path.addQuadCurve(to: p3, controlPoint: p2)
        path.addLine(to: p4)
        path.addLine(to: p5)
        path.addLine(to: p1)
        path.close()
    
        self.shapeLayer.path = path.cgPath
    
        if !self.didSetGradient {
                self.layer.insertSublayer(shapeLayer, at: 0)
    
            self.gradient = CustomColors.blueGreenBackgroundGradient(frame: path.bounds)
            self.gradient.mask = shapeLayer
            self.gradient.masksToBounds = true
            self.layer.insertSublayer(self.gradient, at: 0)
    
            self.didSetGradient = true
        }       
    
        self.layer.shadowPath = startPath.cgPath
        self.layer.shadowColor = CustomColors.blueGreen.cgColor
        self.layer.shadowOpacity = 0.5
        self.layer.shadowRadius = 8
        self.layer.shadowOffset = CGSize(width: 0, height: 7)
        self.layer.masksToBounds = false
        self.clipsToBounds = false
    }