Ross Kimes
Ross Kimes

Reputation: 1234

Displaying and array of colors using CGShading

I have an array of CGColors that I need to display across a path. I tried doing this with a CGGradient, but I don't want the colors the blend between between values. It looks like the best solution would be to use a GGShading object, but I am having trouble figuring out exactly how they work. I'm mainly confused about what I need to have for the CGFunction input for the CGShading.

Can someone point me in the right direction on what I would need to make this CGFunction look like to to simply display an array go CGColors on a specified CGPath?

Thanks!

Upvotes: 5

Views: 1417

Answers (2)

kristofkalai
kristofkalai

Reputation: 444

I rewrote the accepted and rewarded answer in Swift 5+, this works exactly on the same way as the previous solution. I remove the comments in order to be the code the minimal that needed, but the called functions etc. are the same.

final class CGShadingCircle: UIView {
    private var shadingFunction: CGFunction?
    private var colors: Colors?

    final class Colors {
        let colors: [UIColor]

        init(colors: [UIColor]) {
            self.colors = colors
        }
    }

    init(colors: [UIColor]) {
        self.colors = Colors(colors: colors)
        super.init(frame: .zero)
        backgroundColor = .clear
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
}

extension CGShadingCircle {
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let shadingFunction = shadingFunction,
              let context = UIGraphicsGetCurrentContext(),
              let shading = CGShading(axialSpace: CGColorSpaceCreateDeviceRGB(),
                                      start: CGPoint(x: bounds.minX, y: bounds.midY),
                                      end: CGPoint(x: bounds.maxX, y: bounds.midY),
                                      function: shadingFunction,
                                      extendStart: true,
                                      extendEnd: true) else { return }
        context.addEllipse(in: bounds)
        context.clip()
        context.drawShading(shading)
        //            CGShadingRelease(shading) 'CGShadingRelease' is unavailable: Core Foundation objects are automatically memory managed
    }
}

extension CGShadingCircle {
    private func commonInit() {
        let callback: CGFunctionEvaluateCallback = { info, inData, outData in
            guard let colors = info?.load(as: Colors.self).colors else { return }
            var colorIndex = Int(inData.pointee * CGFloat(colors.count))
            if colorIndex >= colors.count {
                colorIndex = colors.count - 1
            }
            let color = colors[colorIndex]
            memcpy(outData, color.cgColor.components, 4 * MemoryLayout<CGFloat>.size)
        }

        var callbacks = CGFunctionCallbacks(version: 0, evaluate: callback, releaseInfo: nil)
        var domain: [CGFloat] = [0, 1]
        var range: [CGFloat] = [0, 1, 0, 1, 0, 1, 0, 1]

        shadingFunction = CGFunction(info: &colors,
                                     domainDimension: domain.count / 2,
                                     domain: &domain,
                                     rangeDimension: range.count / 2,
                                     range: &range,
                                     callbacks: &callbacks)
    }
}

Note that CGShadingRelease(shading) is not needed anymore. If you try to use it, Xcode will give you an error: "Core Foundation objects are automatically memory managed."

(CGFunctionEvaluateCallback can be replaced with @convention(c) (UnsafeMutableRawPointer?, UnsafePointer<CGFloat>, UnsafeMutablePointer<CGFloat>) -> (), but the former is way more concise and clear in a Swift codebase.)

And here is a complete example with the exact same output as the Objective-C based solution:

final class ViewController: UIViewController {
    private let colors: [UIColor] = {
        var colors = [UIColor]()
        colors.append(UIColor(red: 1, green: 0, blue: 0, alpha: 1))
        colors.append(UIColor(red: 0, green: 1, blue: 0, alpha: 1))
        colors.append(UIColor(red: 0, green: 0, blue: 1, alpha: 1))
        colors.append(UIColor(red: 1, green: 1, blue: 1, alpha: 1))
        colors.append(UIColor(red: 0, green: 0, blue: 0, alpha: 1))
        return colors
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        let circle = CGShadingCircle(colors: colors)
        view.addSubview(circle)
        circle.translatesAutoresizingMaskIntoConstraints = false
        circle.heightAnchor.constraint(equalToConstant: 200).isActive = true
        circle.widthAnchor.constraint(equalToConstant: 200).isActive = true
        circle.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        circle.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    }
}

Upvotes: 1

Desmond
Desmond

Reputation: 696

Perhaps a little late, so I hope this is still of use to you. I've listed the code for a simple UIView subclass that draws a circle using the shading you described. The code with comments should be self-explanatory.

@implementation CGShadingCircle


// This is the callback of our shading function.
// info:    a pointer to our NSMutableArray of UIColor objects
// inData:  contains a single float that gives is the current position within the gradient
// outData: we fill this with the color to display at the given position
static void CGShadingCallback(void* info, const float* inData, float* outData) {
    // Our colors
    NSMutableArray* colors = (NSMutableArray*)info;
    // Position within the gradient, ranging from 0.0 to 1.0
    CGFloat position = *inData;

    // Find the color that we want to used based on the current position;
    NSUInteger colorIndex = position * [colors count];

    // Account for the edge case where position == 1.0
    if (colorIndex >= [colors count])
        colorIndex = [colors count] - 1;

    // Get our desired color from the array
    UIColor* color = [colors objectAtIndex:colorIndex];

    // Copy the 4 color components (red, green, blue, alpha) to outData
    memcpy(outData, CGColorGetComponents(color.CGColor), 4 * sizeof(CGFloat));  
}


// Set up our colors and shading function
- (void)initInternal {  
    _colors = [[NSMutableArray alloc] init];

    // Creating the colors in this way ensures that the underlying color space is UIDeviceRGBColorSpace
    // and thus has 4 color components: red, green, blue, alpha
    [_colors addObject:[UIColor colorWithRed:1.0f green:0.0f blue:0.0f alpha:1.0f]]; // Red
    [_colors addObject:[UIColor colorWithRed:0.0f green:1.0f blue:0.0f alpha:1.0f]]; // Green
    [_colors addObject:[UIColor colorWithRed:0.0f green:0.0f blue:1.0f alpha:1.0f]]; // Blue
    [_colors addObject:[UIColor colorWithRed:1.0f green:1.0f blue:1.0f alpha:1.0f]]; // White
    [_colors addObject:[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:1.0f]]; // Black

    // Define the shading callbacks
    CGFunctionCallbacks callbacks;
    callbacks.version = 0;                      // Defaults to 0
    callbacks.evaluate = CGShadingCallback;     // This is our color selection function
    callbacks.releaseInfo = NULL;               // Not used

    // As input to our function we want 1 value in the range [0.0, 1.0].
    // This is our position within the 'gradient'.
    size_t domainDimension = 1;
    CGFloat domain[2] = {0.0f, 1.0f};

    // The output of our function is 4 values, each in the range [0.0, 1.0].
    // This is our selected color for the input position.
    // The 4 values are the red, green, blue and alpha components.
    size_t rangeDimension = 4;
    CGFloat range[8] = {0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f};

    // Create the shading function
    _shadingFunction = CGFunctionCreate(_colors, domainDimension, domain, rangeDimension, range, &callbacks);   
}

- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self initInternal];
    }
    return self;
}

- (id)initWithCoder:(NSCoder*)decoder {
    self = [super initWithCoder:decoder];
    if (self) {
        [self initInternal];
    }
    return self;
}

- (void)dealloc {
    [_colors release];
    CGFunctionRelease(_shadingFunction);

    [super dealloc];
}

- (void)drawRect:(CGRect)rect {
    CGRect b = self.bounds;
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    // Create a simple elliptic path
    CGContextAddEllipseInRect(ctx, b);
    // Set the current path as the clipping path
    CGContextClip(ctx);

    // Create our shading using the function that was defined earlier.
    CGColorSpaceRef colorspace = CGColorSpaceCreateDeviceRGB();
    CGShadingRef shading = CGShadingCreateAxial(colorspace,
        CGPointMake(CGRectGetMinX(b), CGRectGetMidY(b)),
        CGPointMake(CGRectGetMaxX(b), CGRectGetMidY(b)),
        _shadingFunction,
        true,
        true);

    // Draw the shading
    CGContextDrawShading(ctx, shading);


    // Cleanup
    CGShadingRelease(shading);
    CGColorSpaceRelease(colorspace);
}

@end

This gives me the following output:

I hope this helps!

Upvotes: 5

Related Questions