32

I searched everywhere but didn't find the solution. I have image 1. How can I programatically tint them with gradient to get images 2 and 3? Here are those images:

images 1, 2, 3

Tints that I applied to them via Photoshop are simple 2-color linear gradients.

And my question is: how can I achieve this effect programatically?


Solution: jrtc27 gave me almost working example. I fixed it (for ARC) and made it reusable (using UIImage's category). Here is it:

- (UIImage *)tintedWithLinearGradientColors:(NSArray *)colorsArr {
    CGFloat scale = self.scale;
    UIGraphicsBeginImageContext(CGSizeMake(self.size.width * scale, self.size.height * scale));
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextTranslateCTM(context, 0, self.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    CGContextSetBlendMode(context, kCGBlendModeNormal);
    CGRect rect = CGRectMake(0, 0, self.size.width * scale, self.size.height * scale);
    CGContextDrawImage(context, rect, self.CGImage);

    // Create gradient

    UIColor *colorOne = [colorsArr objectAtIndex:1]; // top color
    UIColor *colorTwo = [colorsArr objectAtIndex:0]; // bottom color


    NSArray *colors = [NSArray arrayWithObjects:(id)colorOne.CGColor, (id)colorTwo.CGColor, nil];
    CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColors(space, (__bridge CFArrayRef)colors, NULL);

    // Apply gradient

    CGContextClipToMask(context, rect, self.CGImage);
    CGContextDrawLinearGradient(context, gradient, CGPointMake(0,0), CGPointMake(0,self.size.height * scale), 0);
    UIImage *gradientImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return gradientImage;
}
akashivskyy
  • 44,342
  • 16
  • 106
  • 116
  • Kevin O'Neill has a tint category on UIImage in his Useful Bits library, it takes a single UIColor as a parameter to use for the tinting, but you may be able to adapt it to put a gradient background in the drawing context instead of a solid color: https://github.com/kevinoneill/Useful-Bits – BP. Nov 11 '11 at 18:19
  • Yeah, but I don't know how to make a gradient with CG... – akashivskyy Nov 11 '11 at 18:24
  • CGGradient: http://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CGGradient/Reference/reference.html#//apple_ref/doc/uid/TP40004880 – Peter Hosey Nov 11 '11 at 22:05
  • 2
    I found a small bug in the code above. I think that CGContextTranslateCTM(context, 0, self.size.height); should be CGContextTranslateCTM(context, 0, self.size.height*scale); – Ross Kimes Feb 24 '12 at 18:22
  • @RossKimes Yes, I also found this bug. Without this scale factor. The image will be cut off. – Honghao Z Sep 03 '15 at 14:17

5 Answers5

23

Swift 4 version.

import UIKit

extension UIImage {

func tintedWithLinearGradientColors(colorsArr: [CGColor]) -> UIImage {
    UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale);
    guard let context = UIGraphicsGetCurrentContext() else {
        return UIImage()
    }
    context.translateBy(x: 0, y: self.size.height)
    context.scaleBy(x: 1, y: -1)

    context.setBlendMode(.normal)
    let rect = CGRect.init(x: 0, y: 0, width: size.width, height: size.height)

    // Create gradient
    let colors = colorsArr as CFArray
    let space = CGColorSpaceCreateDeviceRGB()
    let gradient = CGGradient(colorsSpace: space, colors: colors, locations: nil)

    // Apply gradient
    context.clip(to: rect, mask: self.cgImage!)
    context.drawLinearGradient(gradient!, start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: self.size.height), options: .drawsAfterEndLocation)
    let gradientImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()

    return gradientImage!
}
}
klauslanza
  • 671
  • 1
  • 6
  • 8
23

EDIT: Here is a version which supports non-retina and retina displays

The method can be used as a category for UIImage

+ (UIImage *)imageWithGradient:(UIImage *)img startColor:(UIColor *)color1 endColor:(UIColor *)color2 {
    UIGraphicsBeginImageContextWithOptions(img.size, NO, img.scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextTranslateCTM(context, 0, img.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    CGContextSetBlendMode(context, kCGBlendModeNormal);
    CGRect rect = CGRectMake(0, 0, img.size.width, img.size.height);
    //CGContextDrawImage(context, rect, img.CGImage);

    // Create gradient
    NSArray *colors = [NSArray arrayWithObjects:(id)color2.CGColor, (id)color1.CGColor, nil];
    CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
    CGGradientRef gradient = CGGradientCreateWithColors(space, (__bridge CFArrayRef)colors, NULL);

    // Apply gradient
    CGContextClipToMask(context, rect, img.CGImage);
    CGContextDrawLinearGradient(context, gradient, CGPointMake(0,0), CGPointMake(0, img.size.height), 0);
    UIImage *gradientImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    CGGradientRelease(gradient);
    CGColorSpaceRelease(space);

    return gradientImage;
}

Edit: added change by sobri

remy
  • 1,511
  • 10
  • 15
  • +1 for fixing the scaling issue with retina images. It looks like your `UIGraphicsBeginImageContextWithOptions()` line fixed my issues. – rwyland Aug 17 '12 at 16:44
  • I found that removing the `CGContextDrawImage`call improved the resulting images. With it included, the images have rough edges (because the original image is drawn beneath the gradient). – sobri Oct 17 '12 at 04:27
  • @sobri good catch, I changed the answer. It was not noticeable in my case where the image and the background had the same color, but with different colors this is a big improvement – remy Nov 01 '12 at 10:17
  • @remy I found one case where you need to keep the underlying image: If you want to apply a tint (ie a gradient with alpha) instead of an opaque gradient, then you still need to draw in the original image. But otherwise, the results are better without :) – sobri Nov 01 '12 at 22:29
  • @remy all these code has to be called from the `drawRect()` method right? – harinsa Feb 05 '15 at 07:31
  • @remy how would the start and end points be for a diagonal (0,0) to (1,1) gradient? – Luchi Parejo Alcazar May 09 '23 at 20:32
12

I believe that the following should work - do comment if it doesn't!

// Load image
UIImage *image = [UIImage imageNamed:@"MyCoolImage.png"];
CGFloat scale = image.scale;
UIGraphicsBeginImageContext(CGSizeMake(image.size.width * scale, image.size.height * scale));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(context, 0, image.size.height);
CGContextScaleCTM(context, 1.0, -1.0);

CGContextSetBlendMode(context, kCGBlendModeMultiply);
CGRect rect = CGRectMake(0, 0, image.size.width * scale, image.size.height * scale);
CGContextDrawImage(context, rect, image.CGImage);

// Create gradient

UIColor *colorOne = ....;
UIColor *colorTwo = ....;

NSArray *colors = [NSArray arrayWithObjects:(id)colorOne.CGColor, (id)colorTwo.CGColor, nil];
CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColors(space, (CFArrayRef)colors, NULL);

// Apply gradient

CGContextClipToMask(context, rect, image.CGImage);
CGContextDrawLinearGradient(context, gradient, CGPointMake(0,0), CGPointMake(0,image.size.height * scale), 0);
CGGradientRelease(gradient);
UIImage *gradientImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

Credit to CoffeeShopped for the basic idea.

jrtc27
  • 8,496
  • 3
  • 36
  • 68
  • There were some errors but I managed to fix them. Thanks very much! – akashivskyy Nov 11 '11 at 19:54
  • For my own knowledge, what were they, please? – jrtc27 Nov 11 '11 at 20:07
  • 1. `UIGraphicsBeginImageContext(CGPointMake(image.size.x * scale, image.size.y * scale));` (there should be .width and .height, not .x, .y + it should be CGRectMake, not CGPointMake) 2. `NSArray *colors = [NSArray arrayWithObjects:colorOne.CGColor, colorTwo.CGColor, nil];` (there should be (id) before colorOne and colorTwo) – akashivskyy Nov 11 '11 at 20:21
  • 1. Ah yes... 2. Really? Well, if it works for you, I can't argue :P – jrtc27 Nov 11 '11 at 20:36
  • The answer by remy worked without fixes for both retina and non-retina displays. – Prometheus Jan 15 '13 at 00:03
  • There should be a CGGradientRelease(gradient) somewhere near the end, no? – CMash Mar 04 '16 at 14:26
5

Swift version (as UIImage extension, using remy's answer ):

extension UIImage {
    func tintedWithLinearGradientColors(colorsArr: [CGColor!]) -> UIImage {
        UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale);
        let context = UIGraphicsGetCurrentContext()
        CGContextTranslateCTM(context, 0, self.size.height)
        CGContextScaleCTM(context, 1.0, -1.0)

        CGContextSetBlendMode(context, kCGBlendModeNormal)
        let rect = CGRectMake(0, 0, self.size.width, self.size.height)

        // Create gradient

        let colors = colorsArr as CFArray
        let space = CGColorSpaceCreateDeviceRGB()
        let gradient = CGGradientCreateWithColors(space, colors, nil)

        // Apply gradient

        CGContextClipToMask(context, rect, self.CGImage)
        CGContextDrawLinearGradient(context, gradient, CGPointMake(0, 0), CGPointMake(0, self.size.height), 0)
        let gradientImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return gradientImage
    }
}
Juanma Reyes
  • 51
  • 1
  • 3
-1

class ViewController: UIViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    
  
    
    //Original image displayed as is
    let imageView = UIImageView()
    imageView.frame = CGRect(x: 50, y: 50, width: 200, height: 200)
    imageView.contentMode = .scaleAspectFit
    view.addSubview(imageView)
    
    let myImageName = "image.png"
    let myImage = UIImage(named: myImageName)
    imageView.image = myImage
    
    //Let's change the image to Gradient Color
    let imageView2 = UIImageView()
    imageView2.frame = CGRect(x: 300, y: 50, width: 200, height: 200)
    imageView2.contentMode = .scaleAspectFit
    view.addSubview(imageView2)
    
    let myImageName2 = "apple_logo.png"
    let myImage2 = UIImage(named: myImageName2)
    imageView2.image = myImage2!.maskWithGradientColor(color: UIColor.red)
            
}

}

extension UIImage { func maskWithGradientColor(color: UIColor) -> UIImage? {

    let maskImage = self.cgImage
    let width = self.size.width
    let height = self.size.height
    let bounds = CGRect(x: 0, y: 0, width: width, height: height)
    
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
    let bitmapContext = CGContext(data: nil,
                                  width: Int(width),
                                  height: Int(height),
                                  bitsPerComponent: 8,
                                  bytesPerRow: 0,
                                  space: colorSpace,
                                  bitmapInfo: bitmapInfo.rawValue)
    
    let locations:[CGFloat] = [0.0, 1.0]
    let bottom = UIColor(red: 1, green: 0, blue: 0, alpha: 1).cgColor
    let top = UIColor(red: 0, green: 1, blue: 0, alpha: 0).cgColor
    let colors = [top, bottom] as CFArray
    let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: locations)
    let startPoint = CGPoint(x: width/2, y: 0)
    let endPoint = CGPoint(x: width/2, y: height)
    
    bitmapContext!.clip(to: bounds, mask: maskImage!)
    bitmapContext!.drawLinearGradient(gradient!, start: startPoint, end: endPoint, options: CGGradientDrawingOptions(rawValue: UInt32(0)))
    
    if let cImage = bitmapContext!.makeImage() {
        let coloredImage = UIImage(cgImage: cImage)
        return coloredImage
    }
    else  {
        return nil
    }
}

}