iOS Find Color at Point Between Two Colors

The problem is that you're not subtracting kBottomThreshold from farenheit.

But let's simplify.

First, we want to map the input temperature to a parameter t in the range [0 ... 1]. Then, we want to map t to an output in the range [kBottomR ... kTopR], and also to an output in the range [kBottomG ... kTopG], and also to an output in the range [kBottomB ... kTopB].

UIColor *colorForDegreesFahrenheit(double fahrenheit) {
    double t = (fahrenheit - kBottomThreshold) / (kTopThreshold - kBottomThreshold);

    // Clamp t to the range [0 ... 1].
    t = MAX(0.0, MIN(t, 1.0));

    double r = kBottomR + t * (kTopR - kBottomR);
    double g = kBottomG + t * (kTopG - kBottomG);
    double b = kBottomB + t * (kTopB - kBottomB);

    return [UIColor colorWithRed:r/255 green:g/255 blue:b/255 alpha:1];
}

Swift - 3.0 && 4.0

extension UIColor {
    func toColor(_ color: UIColor, percentage: CGFloat) -> UIColor {
        let percentage = max(min(percentage, 100), 0) / 100
        switch percentage {
        case 0: return self
        case 1: return color
        default:
            var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
            var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
            guard self.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) else { return self }
            guard color.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) else { return self }

            return UIColor(red: CGFloat(r1 + (r2 - r1) * percentage),
                           green: CGFloat(g1 + (g2 - g1) * percentage),
                           blue: CGFloat(b1 + (b2 - b1) * percentage),
                           alpha: CGFloat(a1 + (a2 - a1) * percentage))
        }
    }
}

Usage:-

let colorRed = UIColor.red
let colorBlue = UIColor.blue

let colorOutput = colorRed.toColor(colorBlue, percentage: 50)

Result

enter image description here


In case your gradient is more complex than a 2 color gradient, you may consider drawing a CGGradientRef into a temporary CGImageRef and directly read RGBA values from the image buffer.

Here is something that I had to do with a 5 gradient stops and colors:

    CGFloat tmpImagewidth = 1000.0f; // Make this bigger or smaller if you need more or less resolution (number of different colors).
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

    // create a gradient
    CGFloat locations[] = { 0.0,
        0.35,
        0.55,
        0.8,
        1.0 };
    NSArray *colors = @[(__bridge id) [UIColor redColor].CGColor,
                        (__bridge id) [UIColor greenColor].CGColor,
                        (__bridge id) [UIColor blueColor].CGColor,
                        (__bridge id) [UIColor yellowColor].CGColor,
                        (__bridge id) [UIColor redColor].CGColor,
                        ];
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
    CGPoint startPoint = CGPointMake(0, 0);
    CGPoint endPoint = CGPointMake(tmpImagewidth, 0);

    // create a bitmap context to draw the gradient to, 1 pixel high.
    CGContextRef context = CGBitmapContextCreate(NULL, tmpImagewidth, 1, 8, 0, colorSpace, kCGImageAlphaPremultipliedLast);

    // draw the gradient into it
    CGContextAddRect(context, CGRectMake(0, 0, tmpImagewidth, 1));
    CGContextClip(context);
    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0);

    // Get our RGB bytes into a buffer with a couple of intermediate steps...
    //      CGImageRef -> CFDataRef -> byte array
    CGImageRef cgImage = CGBitmapContextCreateImage(context);
    CGDataProviderRef provider = CGImageGetDataProvider(cgImage);
    CFDataRef pixelData = CGDataProviderCopyData(provider);

    // cleanup:
    CGGradientRelease(gradient);
    CGColorSpaceRelease(colorSpace);
    CGImageRelease(cgImage);
    CGContextRelease(context);

    const UInt8* data = CFDataGetBytePtr(pixelData);

    // we got all the data we need.
    // bytes in the data buffer are a succession of R G B A bytes

    // For instance, the color of the point 27% in our gradient is:
    CGFloat x = tmpImagewidth * .27;
    int pixelIndex = (int)x * 4; // 4 bytes per color
    UIColor *color = [UIColor colorWithRed:data[pixelIndex + 0]/255.0f
                                     green:data[pixelIndex + 1]/255.0f
                                      blue:data[pixelIndex + 2]/255.0f
                                     alpha:data[pixelIndex + 3]/255.0f];

    // done fetching color data, finally release the buffer
    CGDataProviderRelease(provider);

I am not saying this is better than the "math way" in the answer above, certainly there is a memory and cpu tax that goes into producing the temporary image. The advantage of this however, is that the code complexity stays the same no matter how many gradient stops you need...


Thanks @ramchandra-n I implemented the extension to get the intermediate color from an array of colors by percentage

extension Array where Element: UIColor {
    func intermediate(percentage: CGFloat) -> UIColor {
        let percentage = Swift.max(Swift.min(percentage, 100), 0) / 100
        switch percentage {
        case 0: return first ?? .clear
        case 1: return last ?? .clear
        default:
            let approxIndex = percentage / (1 / CGFloat(count - 1))
            let firstIndex = Int(approxIndex.rounded(.down))
            let secondIndex = Int(approxIndex.rounded(.up))
            let fallbackIndex = Int(approxIndex.rounded())
            
            let firstColor = self[firstIndex]
            let secondColor = self[secondIndex]
            let fallbackColor = self[fallbackIndex]
            
            var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
            var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
            guard firstColor.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) else { return fallbackColor }
            guard secondColor.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) else { return fallbackColor }
            
            let intermediatePercentage = approxIndex - CGFloat(firstIndex)
            return UIColor(
                red: CGFloat(r1 + (r2 - r1) * intermediatePercentage),
                green: CGFloat(g1 + (g2 - g1) * intermediatePercentage),
                blue: CGFloat(b1 + (b2 - b1) * intermediatePercentage),
                alpha: CGFloat(a1 + (a2 - a1) * intermediatePercentage)
            )
        }
    }
}

You can use it to get an intermediate color between two or more colors:

let color = [.green, .yellow, .red].intermediate(percentage: 70)