17

In an app, I draw a curved UIBezierPath an an MKOverlayPathView class to show flight routes. This is the code I am using:

- (UIBezierPath *)pathForOverlayForMapRect:(MKMapRect)mapRect {

    ... bla bla bla ...

    UIBezierPath *path = [UIBezierPath bezierPath];
    [path moveToPoint:s];
    [path addQuadCurveToPoint:e controlPoint:cp1];
    [path addLineToPoint:e2];
    [path addQuadCurveToPoint:s2 controlPoint:cp2];
    [path closePath];

    return path;   
  }
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context{

    self.mapRect = mapRect;

    CGContextSetRGBFillColor(context, 1.0, 1.0, 1.0, 1.0);
    CGContextSetRGBStrokeColor(context, 0.0, 0.0, 0.0, 1.0);
    CGContextSetLineWidth(context, mapRect.size.height/700);
    CGContextSetLineJoin(context, kCGLineJoinRound);
    CGContextSetLineCap(context, kCGLineCapRound);

    CGContextAddPath(context, [self pathForOverlayForMapRect:mapRect].CGPath);

    [self updateTouchablePathForMapRect:mapRect];

    CGContextDrawPath(context, kCGPathFillStroke);

}

This is working just fine but I would like to draw a gradient along that path instead of just a fill color. And this is where it is starting to get very tricky.

I have experimented with CGContextDrawLinearGradient() but it hasn't got me anywhere useful yet.

Till
  • 27,559
  • 13
  • 88
  • 122
freshking
  • 1,824
  • 18
  • 31

2 Answers2

28

The trick is to use the stroke path of the line (CGContextReplacePathWithStrokedPath) and clip it (CGContextClip) to restrict the gradient to the path:

// Create a gradient from white to red
CGFloat colors [] = {
    1.0, 1.0, 1.0, 1.0,
    1.0, 0.0, 0.0, 1.0
};

CGColorSpaceRef baseSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColorComponents(baseSpace, colors, NULL, 2);
CGColorSpaceRelease(baseSpace), baseSpace = NULL;

CGContextSetLineWidth(context, mapRect.size.height/700);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineCap(context, kCGLineCapRound);

CGContextAddPath(context, [self pathForOverlayForMapRect:mapRect].CGPath);
CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);

[self updateTouchablePathForMapRect:mapRect];

// Define the start and end points for the gradient
// This determines the direction in which the gradient is drawn
CGPoint startPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect));
CGPoint endPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));

CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);
CGGradientRelease(gradient), gradient = NULL;
neilco
  • 7,964
  • 2
  • 36
  • 41
  • This answer is perfect! Thank you very much. I removed `CGContextReplacePathWithStrokedPath()` so my path would be filled with the gradient and not only be outlined by it. – freshking Dec 17 '13 at 14:28
  • `CGContextReplacePathWithStrokedPath` is useful if you want to a gradient _along_ the path (as per the question) rather than _inside_ the path. – neilco Dec 17 '13 at 15:04
  • The problem I was getting with 'CGContextReplacePathWithStrokedPath' was that the gradient only applied to the path itself but didn't fill it. Stroke instead of fill. Or did I get something wrong? – freshking Dec 17 '13 at 23:20
  • @freshking You asked along a gradient _along_ a path, which is a stroke. At least now you know how to do both gradient strokes and fills :) – neilco Dec 18 '13 at 05:14
  • Good solution, better than adding the mask & layers. – Gamma-Point Apr 21 '15 at 01:13
  • @neilco http://stackoverflow.com/q/32498195/3767017 have a look on this problem and pls provide the sol. – Anurag Bhakuni Sep 21 '15 at 10:55
  • 6
    Correct me if I'm wrong, but this doesn't apply a gradient along the path but uses the path as a sort of cookie cutter to take a section of a linear gradient. The gradient actually follows a straight path. Is there a way to apply the gradient to the path. To illustrate what I mean, suppose the path is a loop, starting with white at the beginning and dark at the end. The difference is that a linear gradient would result in a path that gets darker, then lighter again through the loop, then darker again to the end. A gradient along the path would start white and change to black by the end. – Victor Engel Oct 11 '17 at 17:13
1

In Swift 3 this can be achieved using the code below. This example is for a straight line but the same principles should apply.

    let startPoint = CGPoint(x:100, y:100)
    let endPoint = CGPoint(x: 300, y:400)

    let context = UIGraphicsGetCurrentContext()!
    context.setStrokeColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0);
    // create a line
    context.move(to: startPoint)
    context.addLine(to: endPoint)
    context.setLineWidth(4)
    // use the line created above as a clipping mask
    context.replacePathWithStrokedPath()
    context.clip()

    // create a gradient
    let locations: [CGFloat] = [ 0.0, 0.5 ]

    let colors = [UIColor.green.cgColor,
                  UIColor.white.cgColor]

    let colorspace = CGColorSpaceCreateDeviceRGB()

    let gradient = CGGradient(colorsSpace: colorspace,
                              colors: colors as CFArray, locations: locations)

    let gradientStartPoint = CGPoint(x: rect.midX, y: rect.minY)
    let gradientEndPoint = CGPoint(x: rect.midX, y: rect.maxY)

    context.drawLinearGradient(gradient!,
                                start: gradientStartPoint, end: gradientEndPoint,
                                options: .drawsBeforeStartLocation)
    UIGraphicsEndImageContext()
keith.g
  • 900
  • 9
  • 19