Search code examples
objective-cmacoscore-graphicsqr-codensgradient

CGContextSetFill with gradient


I currently have a function where I draw a QR code

+ (void)drawQRCode:(QRcode *)code context:(CGContextRef)ctx size:(CGFloat)size {}

Currently, I can set the color including the colorspace using the following:

CGContextSetFillColorWithColor(ctx, [theColor colorUsingColorSpaceName:NSCalibratedWhiteColorSpace].CGColor);

I am trying to implement the ability to create a gradient I've converted two NSColors to an NSGradient - there doesn't appear to be any way of getting a gradient as an NSColor so I'm wondering what would be the best way of setting the fill of the context with a gradient?

Thanks in advance for any suggestions!


Solution

  • CGContext has drawLinearGradient

    https://developer.apple.com/documentation/coregraphics/cgcontext/1454782-drawlineargradient

    and drawRadialGradient

    https://developer.apple.com/documentation/coregraphics/cgcontext/1455923-drawradialgradient

    To use them to fill a shape, put the shape's path into the context, then use the shape as a clipping path before drawing the gradient.

    Here's a drawRect from a view that demonstrates the technique:

    - (void) drawRect: (CGRect) rect {
        CGColorRef myColors[3] = {
            [[UIColor redColor] CGColor],
            [[UIColor yellowColor] CGColor],
            [[UIColor blueColor] CGColor] };
    
        CGFloat locations[3] = { 0.0, 0.25, 1.0 };
    
        CGRect circleRect = CGRectInset([self bounds], 20, 20);
        CGFloat circleRadius = circleRect.size.width / 2.0;
    
        CGContextRef cgContext = UIGraphicsGetCurrentContext();
    
        CGContextSaveGState(cgContext);
        CGContextSetFillColorWithColor(cgContext, [[UIColor blackColor] CGColor]);
        CGContextFillRect(cgContext, [self bounds]);
        CGContextRestoreGState(cgContext);
    
        CGContextSaveGState(cgContext);
        CGContextAddEllipseInRect(cgContext, circleRect);
        CGContextClip(cgContext);
    
        CGContextTranslateCTM(cgContext, CGRectGetMidX(circleRect), CGRectGetMidY(circleRect));
        CGContextRotateCTM(cgContext, M_PI / 3.0);
    
    
        CFArrayRef colors = CFArrayCreate(nil, (const void**) myColors, 3, &kCFTypeArrayCallBacks);
        CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
        CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, colors, locations);
        CFRelease(colors);
        CFRelease(colorSpace);
    
        CGContextDrawLinearGradient(cgContext, gradient, CGPointMake(-circleRadius, 0),
                                    CGPointMake(circleRadius, 0), kCGGradientDrawsAfterEndLocation + kCGGradientDrawsBeforeStartLocation);
        CFRelease(gradient);
        CGContextRestoreGState(cgContext);
    }
    

    Here's an equivalent playground in Swift.

    import UIKit
    import PlaygroundSupport
    
    class CircleGradientView : UIView {
        override func draw(_ rect: CGRect) {
    
            let colors  = [
                UIColor.red.cgColor,
                UIColor.yellow.cgColor, // Yellow
                UIColor.blue.cgColor  // Blue
            ]
    
            let colorSpace = CGColorSpace(name: CGColorSpace.sRGB)!
    
    
            let locations : [CGFloat] = [0.0, 0.25, 1.0]
            let gradient = CGGradient(colorsSpace: colorSpace,
                                      colors: colors as CFArray,
                                      locations: locations)
    
            if let context = UIGraphicsGetCurrentContext() {
                let circleRect = self.bounds.insetBy(dx: 20, dy: 20)
                let circleRadius = circleRect.width / 2.0
    
                context.saveGState()
                context.addEllipse(in: circleRect)
                context.clip()
    
                context.translateBy(x: circleRect.midX, y: circleRect.midY)
                context.rotate(by: .pi / 3.0)
    
                context.drawLinearGradient(gradient!,
                                           start: CGPoint(x: -circleRadius, y: 0),
                                           end: CGPoint(x: circleRadius, y: 0),
                                           options: [.drawsAfterEndLocation, .drawsBeforeStartLocation])
               context.restoreGState()
            }
        }
    }
    
    let myView = CircleGradientView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
    PlaygroundSupport.PlaygroundPage.current.liveView = myView