33

How can I set the color of a templated image that is an attachment on an attributed string?

Background:

I've got a UILabel and I'm setting its attributedText to an NSAttributedString. The NSAttributedString includes an NSTextAttachment with a small image. Now I want to make my image color match the text color and I can't figure out how to make it work.

I would normally expect to color the image by setting its rendering mode to UIImageRenderingModeAlwaysTemplate and then setting the tintColor on the containing UIView. I've tried setting the tintColor on my UILabel but that has no effect.

Here's my code. It's in Ruby (RubyMotion) so the syntax might look a little funny, but it maps 1:1 with Objective C.

attachment = NSTextAttachment.alloc.initWithData(nil, ofType: nil)
attachment.image = UIImage.imageNamed(icon_name).imageWithRenderingMode(UIImageRenderingModeAlwaysTemplate)

label_string = NSMutableAttributedString.attributedStringWithAttachment(attachment)
label_string.appendAttributedString(NSMutableAttributedString.alloc.initWithString('my text', attributes: { NSFontAttributeName => UIFont.preferredFontForTextStyle(UIFontTextStyleFootnote), NSForegroundColorAttributeName => foreground_color }))

label = UILabel.alloc.initWithFrame(CGRectZero)
label.tintColor = foreground_color
label.attributedText = label_string
label.textAlignment = NSTextAlignmentCenter
label.numberOfLines = 0
Ghazgkull
  • 934
  • 1
  • 6
  • 15
  • Have you found any answers to this that don't involve drawing the image before adding it as a text attachment? I would love to have a simple way of doing this. – Tim Johnsen Apr 29 '15 at 04:54
  • Unfortunately not. In my case, I had a string that was sometimes white on a dark background and sometimes black on a light background... I ended up just changing the attachment image to a neutral gray that worked on both backgrounds. – Ghazgkull Apr 30 '15 at 18:09

7 Answers7

41

It seems that there's a bug in UIKit. There's a workaround for that ;]

For some reason you need to append empty space before image attachment to make it work properly with UIImageRenderingModeAlwaysTemplate.

So your snippet would look like that (mine is in ObjC):

- (NSAttributedString *)attributedStringWithValue:(NSString *)string image:(UIImage *)image {
    NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
    attachment.image = image;

    NSAttributedString *attachmentString = [NSAttributedString attributedStringWithAttachment:attachment];
    NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithAttributedString:[[NSAttributedString alloc] initWithString:@" "]];
    [mutableAttributedString appendAttributedString:attachmentString];
    [mutableAttributedString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:0] range:NSMakeRange(0, mutableAttributedString.length)]; // Put font size 0 to prevent offset
    [mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(0, mutableAttributedString.length)];
    [mutableAttributedString appendAttributedString:[[NSAttributedString alloc] initWithString:@" "]];

    NSAttributedString *ratingText = [[NSAttributedString alloc] initWithString:string];
    [mutableAttributedString appendAttributedString:ratingText];
    return mutableAttributedString;
}
blazejmar
  • 1,000
  • 7
  • 10
  • This does appear to be the case, and this is useful information. I think I'll go with the tinting image approach, but it's good to know. – David Snabel-Caunt Apr 27 '16 at 15:17
  • 1
    I found that `NSForegroundColorAttributeName` wasn't tinting the attached image. Instead it was taking it from the `textColor` property of the `UILabel`, even though that was being overridden in the attributed string. – Samah Aug 01 '16 at 05:20
  • 4
    You can use an empty string instead of space, as the space is added to the layout. [[NSMutableAttributedString alloc] initWithAttributedString:[[NSAttributedString alloc] initWithString:@"\0"]]; – railwayparade Feb 22 '17 at 01:27
  • The main thing is that one must start tinted attributed string with attachment from any letter (but not that attachment). The solution and the comment of @railwayparade explains this workaround. – malex Jul 13 '18 at 20:53
  • Starting with an empty string (`"\0"`) doesn't seem to work for me. I have to replace it with an actual space (`" "`) for the image to be tinted. – Rodrigo Sieiro Jul 17 '19 at 19:46
16

I have good experience with using the library UIImage+Additions when tinting UIImage instances. Specially check section IV.

If adding a third-party library is not an option, here is something to get you started:

- (UIImage *)colorImage:(UIImage *)image color:(UIColor *)color
{
    UIGraphicsBeginImageContextWithOptions(image.size, NO, [UIScreen mainScreen].scale);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextTranslateCTM(context, 0, image.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);
    CGRect rect = CGRectMake(0, 0, image.size.width, image.size.height);

    CGContextSetBlendMode(context, kCGBlendModeNormal);
    CGContextDrawImage(context, rect, image.CGImage);
    CGContextSetBlendMode(context, kCGBlendModeSourceIn);
    [color setFill];
    CGContextFillRect(context, rect);


    UIImage *coloredImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return coloredImage;
}

This will make a UIImage go from:

UIImage before tinting

To

UIImage after tinting

Update: Swift version:

extension UIImage {
    func colorImage(with color: UIColor) -> UIImage? {
        guard let cgImage = self.cgImage else { return nil }
        UIGraphicsBeginImageContext(self.size)
        let contextRef = UIGraphicsGetCurrentContext()

        contextRef?.translateBy(x: 0, y: self.size.height)
        contextRef?.scaleBy(x: 1.0, y: -1.0)
        let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height)

        contextRef?.setBlendMode(CGBlendMode.normal)
        contextRef?.draw(cgImage, in: rect)
        contextRef?.setBlendMode(CGBlendMode.sourceIn)
        color.setFill()
        contextRef?.fill(rect)

        let coloredImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return coloredImage
    }
}
Chackle
  • 2,161
  • 13
  • 30
Steffen D. Sommer
  • 2,788
  • 2
  • 22
  • 47
  • Thanks for the pointer. Hoping I don't have to bring in an entire new library to solve this, though. – Ghazgkull Mar 13 '15 at 20:46
  • @Ghazgkull Added a code example to get your started. Let me know more about how you want to tint if this is not how you wanted to do it. – Steffen D. Sommer Mar 13 '15 at 21:30
  • I would normally tint an image by simply calling UIImage.imageWithRenderingMode(UIImageRenderingModeAlwaysTemplate) and then setting the tintColor of the containing UIView. It's just two lines of code. The only wrinkle here is that my UIImage is inside an NSAttributedString, so I don't have access to the immedate UIView that contains it. – Ghazgkull Mar 13 '15 at 21:52
  • By the way, @Ghazgkull, the entire library linked is only 49 KB! Including it gives a lot of function for a little size :) – Ben Leggiero Dec 29 '15 at 21:13
  • 4
    I noticed the image gets a bit blurry after applying these changes. Any idea how to fix that? I'm using a pdf too. – C0D3 Feb 13 '19 at 21:25
5

I use this NSMutableAttributedString extension for Swift.

extension NSMutableAttributedString {
    func addImageAttachment(image: UIImage, font: UIFont, textColor: UIColor, size: CGSize? = nil) {
        let textAttributes: [NSAttributedString.Key: Any] = [
            .strokeColor: textColor,
            .foregroundColor: textColor,
            .font: font
        ]

        self.append(
            NSAttributedString.init(
                //U+200C (zero-width non-joiner) is a non-printing character. It will not paste unnecessary space.
                string: "\u{200c}",
                attributes: textAttributes
            )
        )

        let attachment = NSTextAttachment()
        attachment.image = image.withRenderingMode(.alwaysTemplate)
        //Uncomment to set size of image. 
        //P.S. font.capHeight sets height of image equal to font size.
        //let imageSize = size ?? CGSize.init(width: font.capHeight, height: font.capHeight)
        //attachment.bounds = CGRect(
        //    x: 0,
        //    y: 0,
        //    width: imageSize.width,
        //    height: imageSize.height
        //)
        let attachmentString = NSMutableAttributedString(attachment: attachment)
        attachmentString.addAttributes(
            textAttributes,
            range: NSMakeRange(
                0,
                attachmentString.length
            )
        )
        self.append(attachmentString)
    }
}

This is how to use it.

let attributedString = NSMutableAttributedString()
if let image = UIImage.init(named: "image") {
    attributedString.addImageAttachment(image: image, font: .systemFont(ofSize: 14), textColor: .red)
}

You can also change addImageAttachment's parameter image: UIImage to image: UIImage? and check the nullability in extension.

shim
  • 7,170
  • 10
  • 62
  • 95
Zhebzhik Babich
  • 901
  • 8
  • 21
3

On iOS 12 we'll need to insert a character before the image and set the foreground color on that character. However, on iOS 13 we can set the foreground color directly on the NSAttributedString that contains our NSTextAttachment.

I have tested the following extension on iOS 12.3 and iOS 13.3.1

extension NSMutableAttributedString {
    @discardableResult
    func sbs_append(_ image: UIImage, color: UIColor? = nil) -> Self {
        let attachment = NSTextAttachment()
        attachment.image = image
        let attachmentString = NSAttributedString(attachment: attachment)
        if let color = color {
            if #available(iOS 13, *) {} else {
                // On iOS 12 we need to add a character with a foreground color before the image,
                // in order for the image to get a color.
                let colorString = NSMutableAttributedString(string: "\0")
                colorString.addAttributes([.foregroundColor: color], range: NSRange(location: 0, length: colorString.length))
                append(colorString)
            }
            let attributedString = NSMutableAttributedString(attributedString: attachmentString)
            if #available(iOS 13, *) {
                // On iOS 13 we can set the foreground color of the image.
                attributedString.addAttributes([.foregroundColor: color], range: NSRange(location: 0, length: attributedString.length))
            }
            append(attributedString)
        } else {
            append(attachmentString)
        }
        return self
    }
}
simonbs
  • 7,543
  • 12
  • 64
  • 112
1

The solution by @blazejmar works, but is unnecessary. All you need to do for this to work is set the color after the attributed strings have been connected. Here's an example.

NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
attachment.image = [[UIImage imageNamed:@"ImageName"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];

NSAttributedString *attachmentString = [NSAttributedString attributedStringWithAttachment:attachment];

NSString *string = @"Some text ";
NSRange range2 = NSMakeRange(string.length - 1, attachmentString.length);

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string];
[attributedString appendAttributedString:attachmentString];
[attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:range2];
self.label.attributedText = attributedString;
cnotethegr8
  • 6,160
  • 7
  • 58
  • 91
  • 1
    It works because you start string not from an attachment. See my comment at @blazejmar solution. – malex Jul 13 '18 at 20:54
0

I found a better solution. Make sure that plain text is in the first item. If the NSTextAttachment (image) is the first item, you can insert a space string before the NSTextAttachment.

// create image attachment
NSTextAttachment *imageAttachment = [[NSTextAttachment alloc] init];
imageAttachment.image = [[UIImage imageNamed:@"ImageName"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
NSAttributedString *imageAttchString = [NSAttributedString attributedStringWithAttachment:attachment];

// create attributedString
NSString *string = @"Some text ";
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:string];

// insert image 
[attributedString insertAttributedString:imageAttchString atIndex:0];
[attributedString insertAttributedString:[[NSAttributedString alloc] initWithString:@" "] atIndex:0];

label.attributedText =  attributedString;

// used
label.textColor = [UIColor redColor];
// or
label.textColor = [UIColor greenColor];
shim
  • 7,170
  • 10
  • 62
  • 95
James
  • 1
  • 1
-3

use UIImageRenderingModeAlwaysOriginal for original image color. UIImageRenderingModeAlwaysTemplate + set tint color for custom color.

Trung Phan
  • 885
  • 9
  • 17