Search code examples
iosswiftviewuibezierpath

Swift Bezierpath doesnt response after addLine Method


I'm trying to draw of my view with using bezierPath and my code is ?

    class auctionCollectionView : UIView{
    let gradient = CAGradientLayer()

    
private var shapeLayer: CALayer?


private func addShape() {
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = mys2().cgPath
    shapeLayer.strokeColor = UIColor.lightGray.cgColor
     
    self.shapeLayer = shapeLayer
    
    let colorTop =  UIColor(hexFromString: "#101820").cgColor
    let colorBottom = UIColor(hexFromString: "#101820").cgColor
    let gradientLayer = CAGradientLayer()
    
    gradientLayer.mask = shapeLayer
    gradientLayer.colors = [colorTop, colorBottom]
    gradientLayer.locations = [0.0, 1.0]
    gradientLayer.frame = mys2().bounds
         
    self.layer.insertSublayer(gradientLayer, at:0)
}
override func draw(_ rect: CGRect) {
    self.addShape()
    
}
func mys2() -> UIBezierPath {
  
    let path = UIBezierPath()

    path.move(to: CGPoint(x: 0, y: self.frame.height/4)) // start top left
    path.addQuadCurve(to:  CGPoint(x: self.frame.width , y:self.frame.height/4), controlPoint: CGPoint(x: self.frame.width/2 , y: 0 ))
    path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height * 3/4))
    
    path.addQuadCurve(to:  CGPoint(x: 0, y: self.frame.height * 3/4), controlPoint: CGPoint(x: self.frame.width/2, y:self.frame.height / 2))
    path.addLine(to: CGPoint(x: 0, y: self.frame.height/4))
    path.close()
    return path
 }
}

And the output is looks like enter image description here

My first QuadCurve looks ok but when I try to add this to from maxX (self.frame.width) , maxY ( self.frame.height * 3/4) to minX ( 0) , maxY (self.frame.height * 3/4) with CGPoint(x: self.frame.width/2, y:self.frame.height / 2) it acts like adding line

What did I missing here ? Regards!


Solution

  • Your path seems to be OK but I am worried your problem is what you call when overriding draw method.

    You should either add layers once and manage them later. Or you should use draw rect to draw everything without using layers. In this case I suggest the second one. I am not exactly sure how your full implementation should be but from copying your path and seeing the gradient code it might be something similar to this:

    @IBDesignable class DrawingView: UIView {
        
        @IBInspectable var topColor: UIColor? = .red
        @IBInspectable var bottomColor: UIColor? = .blue
    
        private func mys2() -> UIBezierPath {
          
            let path = UIBezierPath()
    
            path.move(to: CGPoint(x: 0, y: self.frame.height/4)) // start top left
            path.addQuadCurve(to:  CGPoint(x: self.frame.width , y:self.frame.height/4), controlPoint: CGPoint(x: self.frame.width/2 , y: 0 ))
            path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height * 3/4))
            
            path.addQuadCurve(to:  CGPoint(x: 0, y: self.frame.height * 3/4), controlPoint: CGPoint(x: self.frame.width/2, y:self.frame.height / 2))
            path.addLine(to: CGPoint(x: 0, y: self.frame.height/4))
            path.close()
            return path
        }
        
        override func draw(_ rect: CGRect) {
            super.draw(rect)
            
            guard let context = UIGraphicsGetCurrentContext(), let topColor = topColor, let bottomColor = bottomColor else {
                return
            }
            
            // Save because of clipping
            context.saveGState()
            
            // Add clipping from my path
            mys2().addClip()
            
            let locations: [CGFloat] = [0.0, 1.0]
            let colors = [topColor.cgColor, bottomColor.cgColor]
            if let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: locations) {
                context.drawLinearGradient(gradient, start: CGPoint.zero, end: CGPoint(x: 0.0, y: frame.size.height), options: CGGradientDrawingOptions(rawValue: 0))
            }
            
            context.restoreGState()
            
            
        }
        
    }
    

    So instead of mask we can just use clipping which is kind of what mask does in the first place in layer. The gradient code is a little bit different but still similar enough.

    Now note that this method will be called whenever the view needs to redraw. To force a redraw you need to call setNeedsDisplay. So something like the following can be pretty common:

    @IBInspectable var topColor: UIColor? = .red { didSet { refresh() } }
    @IBInspectable var bottomColor: UIColor? = .blue { didSet { refresh() } }
    
    override var frame: CGRect { didSet { refresh() } }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        refresh()
    }
    
    private func refresh() { setNeedsDisplay() }
    

    So basically you force it to redraw when:

    • Any of the colors change
    • Frame is being set (usual case when not using auto-layout)
    • Subviews are being layout (usual case when using auto-layout)

    Note that calling setNeedsDisplay will not force the reload instantly and call draw method. It just marks the component dirty and will redraw it in the next loop. The result is that draw will be called only once even if you call setNeedsDisplay multiple times in the same loop (stack, method...). So there are no performance issues.

    The same can be done in your case with using layers. You need a refresh method whenever something changes. Then this method should either create sublayers or correct the sublayers frames, mask... And the draw method should be removed. So either use one or the other approach but not both.