How do I add a gradient to the text of a UILabel, but not the background?

泪湿孤枕 提交于 2019-11-26 15:51:11

(Skip to bottom for full class source code)

Really useful answers by both Brad Larson and Bach. The second worked for me but it requires an image to be present in advance. I wanted something more dynamic so I combined both solutions into one:

  • draw the desired gradient on a UIImage
  • use the UIImage to set the color pattern

The result works and in the screenshot below you can see some Greek characters rendered fine too. (I have also added a stroke and a shadow on top of the gradient)

Here's the custom init method of my label along with the method that renders a gradient on a UIImage (part of the code for that functionality I got from a blog post I can not find now to reference it):

- (id)initWithFrame:(CGRect)frame text:(NSString *)aText {
    self = [super initWithFrame:frame];
    if (self) {
        self.backgroundColor = [UIColor clearColor];
        self.text = aText;

        self.textColor = [UIColor colorWithPatternImage:[self gradientImage]];

    }
    return self;
}

- (UIImage *)gradientImage
{
    CGSize textSize = [self.text sizeWithFont:self.font];
    CGFloat width = textSize.width;         // max 1024 due to Core Graphics limitations
    CGFloat height = textSize.height;       // max 1024 due to Core Graphics limitations

    // create a new bitmap image context
    UIGraphicsBeginImageContext(CGSizeMake(width, height));

    // get context
    CGContextRef context = UIGraphicsGetCurrentContext();       

    // push context to make it current (need to do this manually because we are not drawing in a UIView)
    UIGraphicsPushContext(context);                             

    //draw gradient    
    CGGradientRef glossGradient;
    CGColorSpaceRef rgbColorspace;
    size_t num_locations = 2;
    CGFloat locations[2] = { 0.0, 1.0 };
    CGFloat components[8] = { 0.0, 1.0, 1.0, 1.0,  // Start color
                            1.0, 1.0, 0.0, 1.0 }; // End color
    rgbColorspace = CGColorSpaceCreateDeviceRGB();
    glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
    CGPoint topCenter = CGPointMake(0, 0);
    CGPoint bottomCenter = CGPointMake(0, textSize.height);
    CGContextDrawLinearGradient(context, glossGradient, topCenter, bottomCenter, 0);

    CGGradientRelease(glossGradient);
    CGColorSpaceRelease(rgbColorspace); 

    // pop context 
    UIGraphicsPopContext();                             

    // get a UIImage from the image context
    UIImage *gradientImage = UIGraphicsGetImageFromCurrentImageContext();

    // clean up drawing environment
    UIGraphicsEndImageContext();

    return  gradientImage;
}

I'll try to complete that UILabel subclass and post it.

EDIT:

The class is done and it's on my GitHub repository. Read about it here!

I was looking for a solution and DotSlashSlash has the answer hidden in one of the comments!

For the sake of completeness, the answer and the simplest solution is:

UIImage *myGradient = [UIImage imageNamed:@"textGradient.png"];
myLabel.textColor   = [UIColor colorWithPatternImage:myGradient];
Brad Larson

The example you provide relies on private text drawing functions that you don't have access to on the iPhone. The author provides an example of how to do this using public API in a subsequent post. His later example uses a gradient image for the color of the text. (Unfortunately, it appears his blog has since been removed, but see Bach's answer here for the approach he used.)

If you still want to draw the gradient for your text color in code, it can be done by subclassing UILabel and overriding -drawRect: to have code like the following within it:

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0.0f, self.bounds.size.height);
CGContextScaleCTM(context, 1.0f, -1.0f);

CGContextSelectFont(context, "Helvetica", 20.0f, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextClip);
CGContextSetTextPosition(context, 0.0f, round(20.0f / 4.0f));
CGContextShowText(context, [self.text UTF8String], strlen([self.text UTF8String]));

CGContextClip(context);

CGGradientRef gradient;
CGColorSpaceRef rgbColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 1.0, 1.0, 1.0, 1.0,  // Start color
    1.0, 1.0, 1.0, 0.1 }; // End color

rgbColorspace = CGColorSpaceCreateDeviceRGB();
gradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);

CGRect currentBounds = self.bounds;
CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));
CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);

CGGradientRelease(gradient);
CGColorSpaceRelease(rgbColorspace);         

CGContextRestoreGState(context);

One shortcoming of this approach is that the Core Graphics functions I use don't handle Unicode text properly.

What the code does is it flips the drawing context vertically (the iPhone inverts the normal Quartz coordinate system on for the Y axis), sets the text drawing mode to intersect the drawn text with the clipping path, clips the area to draw to the text, and then draws a gradient. The gradient will only fill the text, not the background.

I tried using NSString's -drawAtPoint: method for this, which does support Unicode, but all the characters ran on top of one another when I switched the text mode to kCGTextClip.

SWIFT 3+

This solution is based on @Dimitris's answer. It is an extension on the UILabel class that will create a gradient over the label's text per your passed startColor and endColor. The UILabel extension is below:

extension UILabel {

    func applyGradientWith(startColor: UIColor, endColor: UIColor) -> Bool {

        var startColorRed:CGFloat = 0
        var startColorGreen:CGFloat = 0
        var startColorBlue:CGFloat = 0
        var startAlpha:CGFloat = 0

        if !startColor.getRed(&startColorRed, green: &startColorGreen, blue: &startColorBlue, alpha: &startAlpha) {
            return false
        }

        var endColorRed:CGFloat = 0
        var endColorGreen:CGFloat = 0
        var endColorBlue:CGFloat = 0
        var endAlpha:CGFloat = 0

        if !endColor.getRed(&endColorRed, green: &endColorGreen, blue: &endColorBlue, alpha: &endAlpha) {
            return false
        }

        let gradientText = self.text ?? ""

        let name:String = NSFontAttributeName
        let textSize: CGSize = gradientText.size(attributes: [name:self.font])
        let width:CGFloat = textSize.width
        let height:CGFloat = textSize.height

        UIGraphicsBeginImageContext(CGSize(width: width, height: height))

        guard let context = UIGraphicsGetCurrentContext() else {
            UIGraphicsEndImageContext()
            return false
        }

        UIGraphicsPushContext(context)

        let glossGradient:CGGradient?
        let rgbColorspace:CGColorSpace?
        let num_locations:size_t = 2
        let locations:[CGFloat] = [ 0.0, 1.0 ]
        let components:[CGFloat] = [startColorRed, startColorGreen, startColorBlue, startAlpha, endColorRed, endColorGreen, endColorBlue, endAlpha]
        rgbColorspace = CGColorSpaceCreateDeviceRGB()
        glossGradient = CGGradient(colorSpace: rgbColorspace!, colorComponents: components, locations: locations, count: num_locations)
        let topCenter = CGPoint.zero
        let bottomCenter = CGPoint(x: 0, y: textSize.height)
        context.drawLinearGradient(glossGradient!, start: topCenter, end: bottomCenter, options: CGGradientDrawingOptions.drawsBeforeStartLocation)

        UIGraphicsPopContext()

        guard let gradientImage = UIGraphicsGetImageFromCurrentImageContext() else {
            UIGraphicsEndImageContext()
            return false
        }

        UIGraphicsEndImageContext()

        self.textColor = UIColor(patternImage: gradientImage)

        return true
    }

}

And usage:

let text = "YAAASSSSS!"
label.text = text
if label.applyGradientWith(startColor: .red, endColor: .blue) {
    print("Gradient applied!")
}
else {
    print("Could not apply gradient")
    label.textColor = .black
}


SWIFT 2

class func getGradientForText(text: NSString) -> UIImage {

    let font:UIFont = UIFont(name: "YourFontName", size: 50.0)!
    let name:String = NSFontAttributeName
    let textSize: CGSize = text.sizeWithAttributes([name:font])
    let width:CGFloat = textSize.width         // max 1024 due to Core Graphics limitations
    let height:CGFloat = textSize.height       // max 1024 due to Core Graphics limitations

    //create a new bitmap image context
    UIGraphicsBeginImageContext(CGSizeMake(width, height))

    // get context
    let context = UIGraphicsGetCurrentContext()

    // push context to make it current (need to do this manually because we are not drawing in a UIView)
    UIGraphicsPushContext(context!)

    //draw gradient
    let glossGradient:CGGradientRef?
    let rgbColorspace:CGColorSpaceRef?
    let num_locations:size_t = 2
    let locations:[CGFloat] = [ 0.0, 1.0 ]
    let components:[CGFloat] = [(202 / 255.0), (197 / 255.0), (52 / 255.0), 1.0,  // Start color
                                (253 / 255.0), (248 / 255.0), (101 / 255.0), 1.0] // End color
    rgbColorspace = CGColorSpaceCreateDeviceRGB();
    glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
    let topCenter = CGPointMake(0, 0);
    let bottomCenter = CGPointMake(0, textSize.height);
    CGContextDrawLinearGradient(context, glossGradient, topCenter, bottomCenter, CGGradientDrawingOptions.DrawsBeforeStartLocation);

    // pop context
    UIGraphicsPopContext();

    // get a UIImage from the image context
    let gradientImage = UIGraphicsGetImageFromCurrentImageContext();

    // clean up drawing environment
    UIGraphicsEndImageContext();

    return  gradientImage;
}

Props to @Dimitris

Here's what I'm doing in Swift 3

override func viewDidLoad() {
    super.viewDidLoad()
    timerLabel.textColor = UIColor(patternImage: gradientImage(size: timerLabel.frame.size, color1: CIColor(color: UIColor.green), color2: CIColor(color: UIColor.red), direction: .Left))
}

func gradientImage(size: CGSize, color1: CIColor, color2: CIColor, direction: GradientDirection = .Up) -> UIImage {

    let context = CIContext(options: nil)
    let filter = CIFilter(name: "CILinearGradient")
    var startVector: CIVector
    var endVector: CIVector

    filter!.setDefaults()

    switch direction {
    case .Up:
        startVector = CIVector(x: size.width * 0.5, y: 0)
        endVector = CIVector(x: size.width * 0.5, y: size.height)
    case .Left:
        startVector = CIVector(x: size.width, y: size.height * 0.5)
        endVector = CIVector(x: 0, y: size.height * 0.5)
    case .UpLeft:
        startVector = CIVector(x: size.width, y: 0)
        endVector = CIVector(x: 0, y: size.height)
    case .UpRight:
        startVector = CIVector(x: 0, y: 0)
        endVector = CIVector(x: size.width, y: size.height)
    }

    filter!.setValue(startVector, forKey: "inputPoint0")
    filter!.setValue(endVector, forKey: "inputPoint1")
    filter!.setValue(color1, forKey: "inputColor0")
    filter!.setValue(color2, forKey: "inputColor1")

    let image = UIImage(cgImage: context.createCGImage(filter!.outputImage!, from: CGRect(x: 0, y: 0, width: size.width, height: size.height))!)
    return image
}

Swift 4.1

class GradientLabel: UILabel {
    var gradientColors: [CGColor] = []

    override func drawText(in rect: CGRect) {
        if let gradientColor = drawGradientColor(in: rect, colors: gradientColors) {
            self.textColor = gradientColor
        }
        super.drawText(in: rect)
    }

    private func drawGradientColor(in rect: CGRect, colors: [CGColor]) -> UIColor? {
        let currentContext = UIGraphicsGetCurrentContext()
        currentContext?.saveGState()
        defer { currentContext?.restoreGState() }

        let size = rect.size
        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
                                        colors: colors as CFArray,
                                        locations: nil) else { return nil }

        let context = UIGraphicsGetCurrentContext()
        context?.drawLinearGradient(gradient,
                                    start: CGPoint.zero,
                                    end: CGPoint(x: size.width, y: 0),
                                    options: [])
        let gradientImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        guard let image = gradientImage else { return nil }
        return UIColor(patternImage: image)
    }
}

Usage:

label.gradientColors = [UIColor.blue.cgColor, UIColor.red.cgColor]

You could sub-class out UILable and do the draw method yourself. That would probably be the more difficult approach, there might be an easier way.

Simplest Swift 3 Solution

Add an image to your project assets or create one programmatically then do the following:

let image = UIImage(named: "myGradient.png")!
label.textColor = UIColor.init(patternImage: image)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!