Center NSTextAttachment image next to single line UILabel

Solution 1:

You can use the capHeight of the font.

Objective-C

NSTextAttachment *icon = [[NSTextAttachment alloc] init];
UIImage *iconImage = [UIImage imageNamed:@"icon.png"];
[icon setBounds:CGRectMake(0, roundf(titleFont.capHeight - iconImage.size.height)/2.f, iconImage.size.width, iconImage.size.height)];
[icon setImage:iconImage];
NSAttributedString *iconString = [NSAttributedString attributedStringWithAttachment:icon];
[titleText appendAttributedString:iconString];

Swift

let iconImage = UIImage(named: "icon.png")!
var icon = NSTextAttachment()
icon.bounds = CGRect(x: 0, y: (titleFont.capHeight - iconImage.size.height).rounded() / 2, width: iconImage.size.width, height: iconImage.size.height)
icon.image = iconImage
let iconString = NSAttributedString(attachment: icon)
titleText.append(iconString)

The attachment image is rendered on the baseline of the text. And the y axis of it is reversed like the core graphics coordinate system. If you want to move the image upward, set the bounds.origin.y to positive.

The image should be aligned vertically center with the capHeight of the text. So we need to set the bounds.origin.y to (capHeight - imageHeight)/2.

Avoiding some jagged effect on the image, we should round the fraction part of the y. But fonts and images are usually small, even 1px difference makes the image looks like misaligned. So I applied the round function before dividing. It makes the fraction part of the y value to .0 or .5

In your case, the image height is larger than the capHeight of the font. But you can use the same way. The offset y value will be negative. And it will be laid out from the below of the baseline.

enter image description here

Solution 2:

Try - [NSTextAttachment bounds]. No subclassing required.

For context, I am rendering a UILabel for use as the attachment image, then setting the bounds like so: attachment.bounds = CGRectMake(0, self.font.descender, attachment.image.size.width, attachment.image.size.height) and baselines of text within label image and text in attributed string line up as desired.

Solution 3:

You can change the rect by subclassing NSTextAttachment and overriding attachmentBoundsForTextContainer:proposedLineFragment:glyphPosition:characterIndex:. Example:

- (CGRect)attachmentBoundsForTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)lineFrag glyphPosition:(CGPoint)position characterIndex:(NSUInteger)charIndex {
    CGRect bounds;
    bounds.origin = CGPointMake(0, -5);
    bounds.size = self.image.size;
    return bounds;
}

It's not a perfect solution. You have to figure out the Y-origin “by eye” and if you change the font or the icon size, you'll probably want to change the Y-origin. But I couldn't find a better way, except by putting the icon in a separate image view (which has its own disadvantages).