Search code examples
swiftuiviewcore-graphicscashapelayercagradientlayer

Issues with UView gradient border with rounded corners


I have created a UIView subclass which has a gradient border and its background is yellow.


class GradientBorderView: UIView {
    

    override func layoutSubviews() {
        super.layoutSubviews()
        addGradientBorder()
    }

    private func addGradientBorder() {
        self.layer.cornerRadius = 24
        self.backgroundColor = UIColor.yellow
        
        // Remove any existing layers
        self.layer.sublayers?.removeAll(where: { $0.name == "gradientBorder" })
        
        // Create the gradient layer
        let gradientLayer = CAGradientLayer()
        gradientLayer.name = "gradientBorder"
        gradientLayer.frame = bounds
        
        // Define the gradient colors
        gradientLayer.colors = [UIColor.red.withAlphaComponent(0.2).cgColor, UIColor.black.withAlphaComponent(0.2).cgColor]
        
        
        // Create a shape layer that defines the rounded path and border width
        let shapeLayer = CAShapeLayer()
        shapeLayer.lineWidth = 4  // gradient border width
        shapeLayer.fillColor = UIColor.clear.cgColor //fill area will be transparent during masking
        shapeLayer.strokeColor = UIColor.black.cgColor //stroke area will be visible during masking
        
        //Half of the line width is drawn inside and other half of the line width is drawn outside the rectangle.
        //Thats why oringal reactangle was shrunk from all side by half of the line width
        shapeLayer.path = UIBezierPath(roundedRect: bounds.insetBy(dx: 2, dy: 2),
                                       cornerRadius: self.layer.cornerRadius).cgPath
        
        // Mask the gradient layer with the shape layer, only affecting the border
        gradientLayer.mask = shapeLayer
        
        // Add the gradient layer as a sublayer
        layer.addSublayer(gradientLayer)
        
    }
}

screenshot

I am not able to get rid of the extra yellow view part visible around the corners.

The goal is to smoothly align the view layer with the gradient layer around the corners.


Solution

  • Another approach ...

    • set the background to clear
    • set the layer class of the custom view to CAShapeLayer
    • set the .fillColor and .strokeColor of that layer to yellow (or whatever color you want to use)
    • set the .lineWidth to the same line width used for the gradient border

    Then, set the .path of the base shape layer to the same path as the gradient layer.

    For example:

    class AnotherGradientBorderView: UIView {
        
        // let's use self's layer as a CAShapeLayer
        override class var layerClass: AnyClass { CAShapeLayer.self }
        private var selfLayer: CAShapeLayer { layer as! CAShapeLayer }
    
        // border gradient
        private var borderGradientLayer: CAGradientLayer = CAGradientLayer()
        
        override init(frame: CGRect) {
            super.init(frame: frame)
            commonInit()
        }
        
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            commonInit()
        }
        private func commonInit() {
            
            // background needs to be clear
            self.backgroundColor = .clear
            
            selfLayer.fillColor = UIColor.yellow.cgColor
            selfLayer.strokeColor = UIColor.yellow.cgColor
            selfLayer.lineWidth = 4
            
            borderGradientLayer.colors = [UIColor.red.withAlphaComponent(0.2).cgColor, UIColor.black.withAlphaComponent(0.2).cgColor]
            self.layer.addSublayer(borderGradientLayer)
            
        }
        
        override func layoutSubviews() {
            super.layoutSubviews()
    
            borderGradientLayer.frame = bounds
            
            // Create a shape layer that defines the rounded path and border width
            let shapeLayer = CAShapeLayer()
            shapeLayer.lineWidth = 4  // gradient border width
            shapeLayer.fillColor = UIColor.clear.cgColor //fill area will be transparent during masking
            shapeLayer.strokeColor = UIColor.black.cgColor //stroke area will be visible during masking
            
            //Half of the line width is drawn inside and other half of the line width is drawn outside the rectangle.
            //Thats why oringal reactangle was shrunk from all side by half of the line width
            let pth = UIBezierPath(roundedRect: bounds.insetBy(dx: 2, dy: 2),
                                   cornerRadius: 24.0).cgPath
            
            shapeLayer.path = pth
            
            // Mask the gradient layer with the shape layer, only affecting the border
            borderGradientLayer.mask = shapeLayer
    
            // set the path of self's layer (it is a CAShapeLayer) to
            //  match the path of the border layer
            selfLayer.path = pth
    
        }
    }
    

    Output:

    Result