22

I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect) function. I would like to fill the path with a gradient color. Please can anybody guide me how can I do that.

I need to fill the clip with a gradient color and then stroke the path with black color.

There are some posts in SO which does not solve the problem. For example Swift: Gradient along a bezier path (using CALayers) this post guides how to draw on a layer in UIView but not in a UIBezierPath.

NB: I am working on Swift-3

Community
  • 1
  • 1
Sazzad Hissain Khan
  • 37,929
  • 33
  • 189
  • 256

4 Answers4

34

To answer this question of yours,

I have a UIBezierPath inside my custom UIView draw(_ rect: CGRect) function. I would like to fill the path with a gradient color.

Lets say you have an oval path,

let path = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 100, height: 100))

To create a gradient,

let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]

We need a mask layer for gradient,

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

Now set this shapeLayer as mask of gradient layer and add it to view's layer as subLayer

gradient.mask = shapeMask
yourCustomView.layer.addSublayer(gradient)

Update Create a base layer with stroke and add before creating gradient layer.

let shape = CAShapeLayer()
shape.path = path.cgPath
shape.lineWidth = 2.0
shape.strokeColor = UIColor.black.cgColor
self.view.layer.addSublayer(shape)

let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [UIColor.magenta.cgColor, UIColor.cyan.cgColor]

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

self.view.layer.addSublayer(gradient)
Matt
  • 1,711
  • 18
  • 20
  • thanks for your solution. gradient works but the stroke is not shown. `I need to fill the clip with a gradient color and then stroke the path with black color`. How can i fix this. – Sazzad Hissain Khan Jan 03 '17 at 09:44
  • As I am overriding `UIView`, inside `draw()` func I assume you mean `self.layer` instead of `self.view.layer`. I follow all your updated instructions, works pretty close but its only showing borderlines in black. No strokes in the middle of the graph is showing. @Matt – Sazzad Hissain Khan Jan 03 '17 at 10:43
  • 1
    Thanks for your answer. I have just added `shape.fillColor = UIColor.clear.cgColor` and the problem is solved. – Sazzad Hissain Khan Jan 03 '17 at 11:02
  • Can you also help on that post, http://stackoverflow.com/q/41464101/1084174 – Sazzad Hissain Khan Jan 04 '17 at 12:43
  • Sweet and simple. :) – Kunal Gupta Oct 18 '17 at 17:24
  • For me it doesn't work without setting `shapeMask.strokeColor = UIColor.white.cgColor shapeMask.fillColor = UIColor.clear.cgColor shapeMask.lineWidth = shapeLayer.lineWidth` – Jakub Marek Jul 15 '20 at 08:57
17

You can do this directly in Core Graphics without using CALayer classes. Use bezierPath.addClip() to set the bezier path as the clipping region. Any subsequent drawing commands will be masked to that region.

I use this wrapper function in one of my projects:

func drawLinearGradient(inside path:UIBezierPath, start:CGPoint, end:CGPoint, colors:[UIColor])
{
    guard let ctx = UIGraphicsGetCurrentContext() else { return }

    ctx.saveGState()
    defer { ctx.restoreGState() } // clean up graphics state changes when the method returns

    path.addClip() // use the path as the clipping region

    let cgColors = colors.map({ $0.cgColor })
    guard let gradient = CGGradient(colorsSpace: nil, colors: cgColors as CFArray, locations: nil)
        else { return }

    ctx.drawLinearGradient(gradient, start: start, end: end, options: [])
}
Robin Stewart
  • 3,147
  • 20
  • 29
  • Not only is it useful, but also necessary if one starts from `UIGraphicsBeginImageContext(:)` rather than a `UIView`. – JKaz May 03 '20 at 21:05
  • I find this answer interesting, but as I tried to put it in place I wondered. What CGPoint should I put as argument? Of course I guess you're speaking about the path, but then I don't know how to get these points from it. All I generally do is playing with the CALayer's `strokeEnd`, but then I have a CGFloat ranging from 0...1, not a CGPoint. Could you help me clarify these... points? :) – itMaxence Jun 02 '22 at 10:54
  • @itMaxence In this particular function, the `start` and `end` CGPoints specify how to position the color gradient (they do not affect the position of the clipping path). Also, the method here is specifically *not* using CALayer. – Robin Stewart Jun 03 '22 at 17:02
  • Indeed, not targeted to CALayer my bad! I'm still left with one question to be sure I understood correctly. The path is only used as a mask, so there is no actual way to make, say a specific race route, on a road, to then apply a CGGradient along that path? Unless you subdivide the whole path, to draw linear gradients yourself? – itMaxence Jun 06 '22 at 19:24
  • @itMaxence It sounds like you want a gradient _stroke_ along a path rather than a gradient _fill_. Possibly this is more relevant: https://stackoverflow.com/q/1303855/7488171 – Robin Stewart Jun 07 '22 at 20:19
3

The other answers here don't seem to work (At least in newer iOS versions?)

Here's a working example of a bezier path being created and the stroke being a gradient stroke, adjust the CGPoints and CGColors as needed:

        CGPoint start = CGPointMake(300, 400);
        CGPoint end = CGPointMake(350, 400);
        CGPoint control = CGPointMake(300, 365);
        
        UIBezierPath *path = [UIBezierPath new];
        [path moveToPoint:start];
        [path addQuadCurveToPoint:end controlPoint:control];
        
        CAShapeLayer *shapeLayer = [CAShapeLayer new];
        shapeLayer.path = path.CGPath;
        shapeLayer.strokeColor = [UIColor whiteColor].CGColor;
        shapeLayer.fillColor = [UIColor clearColor].CGColor;
        shapeLayer.lineWidth = 2.0;
        //[self.view.layer addSublayer:shapeLayer];
        
        CGRect gradientBounds = CGRectMake(path.bounds.origin.x-shapeLayer.lineWidth, path.bounds.origin.y-shapeLayer.lineWidth, path.bounds.size.width+shapeLayer.lineWidth*2.0, path.bounds.size.height+shapeLayer.lineWidth*2.0);
        CAGradientLayer *gradientLayer = [CAGradientLayer new];
        gradientLayer.frame = gradientBounds;
        gradientLayer.bounds = gradientBounds;
        gradientLayer.colors = @[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor];
        gradientLayer.mask = shapeLayer;
        [self.view.layer addSublayer:gradientLayer];
Albert Renshaw
  • 17,282
  • 18
  • 107
  • 195
0

Swift 5 version of @AlbertRenshaw's (ObjC) answer, and wrapped in a UIView class. What it displays looks spherical but on it's side. A CGAffineTransform could be applied to rotate it 90º for a cool effect.

import UIKit

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

        let centerPoint = CGPoint(x: frame.width / 2, y: frame.height / 2)
        let radius = frame.height / 2
        let circlePath = UIBezierPath(arcCenter: centerPoint, 
                                      radius: radius, 
                                      startAngle: 0, 
                                      endAngle: .pi * 2, 
                                      clockwise: true)

        let shapeLayer = CAShapeLayer()
        shapeLayer.path         = circlePath.cgPath;
        shapeLayer.strokeColor  = UIColor.green.cgColor
        shapeLayer.lineWidth    = 2.0;

        let gradientBounds = CGRectMake(
            circlePath.bounds.origin.x    - shapeLayer.lineWidth,
            circlePath.bounds.origin.y    - shapeLayer.lineWidth,
            circlePath.bounds.size.width  + shapeLayer.lineWidth * 2.0,
            circlePath.bounds.size.height + shapeLayer.lineWidth * 2.0)
        
        let gradientLayer = CAGradientLayer()
        gradientLayer.frame  = gradientBounds;
        gradientLayer.bounds = gradientBounds;
        gradientLayer.colors = [UIColor.red, UIColor.blue].map { $0.cgColor }
        gradientLayer.mask = shapeLayer;
        self.layer.addSublayer(gradientLayer)
    }
}

Usage in a UIViewController:

override func viewDidLoad() {
    view.backgroundColor = .white
    let sphereView = SphereView()
    sphereView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(sphereView)
    NSLayoutConstraint.activate([
        sphereView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        sphereView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        sphereView.widthAnchor.constraint(equalToConstant: 150),
        sphereView.heightAnchor.constraint(equalToConstant: 150),
    ])
}
clearlight
  • 12,255
  • 11
  • 57
  • 75