How to make a CAGradientLayer follow a CGPath

1.6k Views Asked by At

Given a CAShapeLayer defining a path as in the picture below, I want to add a CAGradientLayer that follows the path of the shape layer.

For example, given a gradient from black->red:

  • the top right rounded piece would be black,
  • and if the slider was at 100, the top left would be red,
  • and if the slider was at 50, then half the slider would be black (as below), and the visible gradient would go from black (top right) to a redish-black at the bottom

The Green should become the gradient

Every previous post I've found does not actually answer this question. For example, because I can only add axial CAGradientLayers, I can kind-of do this (pic below), but you can see it's not correct (the top left ends up becoming black again). How do I make the gradient actually follow the path/mask

enter image description here

2

There are 2 best solutions below

3
matt On BEST ANSWER

For the simple circular shape path, the new conic gradient is great. I was able to get this immediately (this is a conic gradient layer masked by a circle shape layer):

enter image description here

I used red and green so as to show the gradient clearly. This doesn't seem to be identically what you're after, but I can't believe it will be very difficult to achieve your goals now that .conic exists.

class MyGradientView : UIView {
    override class var layerClass : AnyClass { return CAGradientLayer.self }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder:aDecoder)
        let lay = self.layer as! CAGradientLayer
        lay.type = .conic
        lay.startPoint = CGPoint(x:0.5,y:0.5)
        lay.endPoint = CGPoint(x:0.5,y:0)
        lay.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        let shape = CAShapeLayer()
        shape.frame = self.bounds
        shape.strokeColor = UIColor.black.cgColor
        shape.fillColor = UIColor.clear.cgColor
        let b = UIBezierPath(ovalIn: shape.frame.inset(by: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)))
        shape.path = b.cgPath
        shape.lineWidth = 10
        shape.lineCap = .round
        shape.strokeStart = 0.1
        shape.strokeEnd = 0.9
        shape.anchorPoint = CGPoint(x: 0.5, y: 0.5)
        shape.transform = CATransform3DMakeRotation(-.pi/2, 0, 0, 1)
        self.layer.mask = shape
    }
}
0
user1366911 On

Anybody on iOS 12 should go with Matt's suggestion or maybe use SFProgressCircle as DonMag suggested.

My solution: I personally ended up adding two CAShapeLayers, each with a corresponding CAGradientLayer.

By programmatically splitting the slider in half as in the pic below, I was able to apply a top-to-bottom gradient on each side, which gives the effect I'm looking for. It's invisible to the user.

enter image description here