Adding a mask to CAGradientLayer makes UIBezierPath disappear
Asked Answered
S

1

5

I want to add an inner border to a view with a gradient. The following code works and gives me this result

enter image description here

import UIKit

class InnerGradientBorderView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = UIColor.clear
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        backgroundColor = UIColor.clear
    }

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

        addGradientInnerBorder(width: 8, color: FlatWhite())
    }

    func addGradientInnerBorder(width: CGFloat, color: UIColor) {

        // Setup
        let topLeftO = CGPoint(x: 0, y: 0)
        let topLeftI = CGPoint(x: width, y: width)

        let topRightO = CGPoint(x: frame.width, y: 0)
        let topRightI = CGPoint(x: frame.width - width, y: width)

        let bottomLeftO = CGPoint(x: 0, y: frame.height)
        let bottomLeftI = CGPoint(x: width, y: frame.height - width)

        let bottomRightO = CGPoint(x: frame.width, y: frame.height)
        let bottomRightI = CGPoint(x: frame.width - width, y: frame.height - width)

        // Top
        let topPoints = [topLeftO, topLeftI, topRightI, topRightO, topLeftO]
        let topGradientPoints = [CGPoint(x: 0, y: 0), CGPoint(x: 0, y: 1)]
        addGradientToBeizerPath(path: addClosedPathForPoints(points: topPoints), color: color, gradientPoints: topGradientPoints)

        // Left
        let leftPoints = [topLeftO, topLeftI, bottomLeftI, bottomLeftO, topLeftO]
        let leftGradientPoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 0)]
        addGradientToBeizerPath(path: addClosedPathForPoints(points: leftPoints), color: color, gradientPoints: leftGradientPoints)

        // Right
        let rightPoints = [topRightO, topRightI, bottomRightI, bottomRightO, topRightO]
        let rightGradientPoints = [CGPoint(x: 1, y: 0), CGPoint(x: 0, y: 0)]
        addGradientToBeizerPath(path: addClosedPathForPoints(points: rightPoints), color: color, gradientPoints: rightGradientPoints)

        // Bottom
        let bottomPoints = [bottomLeftO, bottomLeftI, bottomRightI, bottomRightO, bottomLeftO]
        let bottomGradientPoints = [CGPoint(x: 0, y: 1), CGPoint(x: 0, y: 0)]
        addGradientToBeizerPath(path: addClosedPathForPoints(points: bottomPoints), color: color, gradientPoints: bottomGradientPoints)
    }

    func addClosedPathForPoints(points: [CGPoint]) -> UIBezierPath? {
        guard points.count == 5 else { return nil }

        let path = UIBezierPath()
        path.move(to: points[0])
        path.addLine(to: points[1])
        path.addLine(to: points[2])
        path.addLine(to: points[3])
        path.addLine(to: points[4])

        path.close()

        return path
    }

    func addGradientToBeizerPath(path: UIBezierPath?, color: UIColor, gradientPoints: [CGPoint]) {
        guard let path = path, gradientPoints.count == 2 else { return }

        let gradient = CAGradientLayer()
        gradient.frame = path.bounds
        gradient.colors = [color.cgColor, UIColor.clear.cgColor]
        gradient.startPoint = gradientPoints[0]
        gradient.endPoint = gradientPoints[1]

//        let shapeMask = CAShapeLayer()
//        shapeMask.path = path.cgPath
//        gradient.mask = shapeMask

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

You will notice that the edges do not look that great.To fix that, I am giving the edges an angle. When I apply a mask to this gradient with this angle, the right and bottom paths disappear like this:

enter image description here

All I am doing here is using some closed bezierPaths and applying a gradient to them. If the gradient has a mask (the commented code is uncommented), two of the paths disappear. I have a feeling that I am not understanding something so hopefully someone here can tell me how to use CAShapeLayer properly.

Stateless answered 20/12, 2017 at 2:9 Comment(4)
I don't understand why you're adding a CAGradientLayer to the view's layer from inside the draw method - that feels wrong to me. Could you explain what you're trying to achieve?Buchbinder
The I am adding this to is resizable. And since the draw method is called whenever the view needs to be drawn or redrawn, I figured I would do the work there. Did you have a better implementation in mind?Stateless
Yeah, layoutSubviews or somewhere. You should only override draw to actually perform rendering, not to change the layer hierarchy. Since you're inserting layers as a result of the draw call, and never removing them that I can see, you're going to end up with hundreds of layers aren't you? I'm actually struggling to understand what you're trying to achieve...Buchbinder
I see your point. Not sure what I was thinking. Let me put this through code review and I will update my question with code that makes more sense. Thank you for your response.Stateless
A
8

This comment to CALayer mask property explains it perfectly:

The mask layer lives in the masked layer's coordinate system just as if it were a sublayer.

In your case, the origin of the right and bottom gradient layer is not at (0, 0) of the enclosing view, but at (frame.width - width, 0) and (frame.height - width, 0) respectively. On the other hand, the coordinates of the points in oshapeMask.path are relative to (0, 0) of the enclosing view.

A possible simple fix is to transform the coordinate system of the shape layer so that it uses the same coordinates as the points of the given path:

    let gradient = CAGradientLayer()
    gradient.frame = path.bounds
    gradient.bounds = path.bounds // <<--- ADDED HERE!
    gradient.colors = [color.cgColor, UIColor.clear.cgColor]
    gradient.startPoint = gradientPoints[0]
    gradient.endPoint = gradientPoints[1]

    let shapeMask = CAShapeLayer()
    shapeMask.path = path.cgPath
    gradient.mask = shapeMask

    self.layer.addSublayer(gradient)
Affixation answered 23/12, 2017 at 15:51 Comment(1)
This fixed the issue. Thank you!Stateless

© 2022 - 2024 — McMap. All rights reserved.