110

I am trying to get the following effect using a UITextView:

enter image description here

Basically I want to insert an image between text. The image can simply just take up 1 row of space so there is no wrapping necessary.

I tried just adding a UIView to the subview:

UIView *pictureView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 25, 25)];
[pictureView setBackgroundColor:[UIColor redColor]];
[self.textView addSubview:pictureView];

But it seems to float over the text and cover it.

I did a bit of reading on exclusion paths which appears to be one way of implementing this. However, I don't want to absolutely position the image - instead, it should flow with the text (similar to how <span> behaves in HTML).

Andy Hin
  • 25,283
  • 37
  • 95
  • 135
  • A few of the answers mention using the image properties on NSTextAttachment and NSTextField but I want to mention that I need a solution which allows me to append a UIView. – Andy Hin Jan 05 '14 at 06:01
  • 6
    Amazing that I just watched the Royal Rumble 2011 this morning (where your image was grabbed from) via the WWE network and here I am happening upon this question today. – bpapa Mar 02 '14 at 15:56
  • Heya, have you got an example of some working code involving TextAttachment? – fatuhoku Jun 24 '14 at 08:56

5 Answers5

182

You'll need to use an attributed string and add the image as instance of NSTextAttachment:

NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:@"like after"];

NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
textAttachment.image = [UIImage imageNamed:@"whatever.png"];

NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment];

[attributedString replaceCharactersInRange:NSMakeRange(4, 1) withAttributedString:attrStringWithImage];
Salman Zaidi
  • 8,426
  • 12
  • 42
  • 60
bilobatum
  • 8,848
  • 6
  • 34
  • 49
28

@bilobatum's code converted to Swift for those in need:

let attributedString = NSMutableAttributedString(string: "like after")

let textAttachment = NSTextAttachment()

textAttachment.image = UIImage(named: "whatever.png")

let attrStringWithImage = NSAttributedString(attachment: textAttachment)

attributedString.replaceCharacters(in: NSMakeRange(4, 1), with: attrStringWithImage)
Prcela
  • 3,875
  • 29
  • 45
Justin Vallely
  • 5,157
  • 1
  • 26
  • 39
20

You could try using NSAttributedString and NSTextAttachment. Take a look at the following link for more details on customising the NSTextAttachment in order to resize the image. http://ossh.com.au/design-and-technology/software-development/implementing-rich-text-with-images-on-os-x-and-ios/

In my example I resize the image to fit the width, in your case you may want to resize the image to match the line height.

Duncan Groenewald
  • 6,956
  • 4
  • 31
  • 59
  • I think this is the closest answer so far. Is it possible to use this same technique with the UIView instead of UIImage? – Andy Hin Jan 05 '14 at 07:04
  • You may be able to with some major work on your own custom classes. NSTextAttachment has a default image attribute and the attachment is stored as part of the NSAttributedString. You probably could create your own subclasses and store whatever you want. I think display is limited to displaying an image so not sure a UIView would be useful. As I recall the layoutManager expects an image. – Duncan Groenewald Jan 05 '14 at 13:36
  • 1
    @AndyHin I haven't tested this myself but one option is possibly to render your `UIView` to a `UIImage` and then add it as an `NSTextAttachment`. In order to render the view to an image check out this question: [http://stackoverflow.com/questions/4334233/how-to-capture-uiview-to-uiimage-without-loss-of-quality-on-retina-display?lq=1](http://stackoverflow.com/questions/4334233/how-to-capture-uiview-to-uiimage-without-loss-of-quality-on-retina-display?lq=1) – Michael Gaylord Mar 24 '14 at 12:53
  • Any new developments with this? – fatuhoku Jun 24 '14 at 10:15
5

Expanding on @bilobatum's answer, and using this category from another question. I cooked this up:

Usage:

UILabel *labelWithImage = [UILabel new];
labelWithImage.text = @"Tap [new-button] to make a new thing!";
NSAttributedString *stringWithImage = [labelWithImage.attributedText attributedStringByReplacingOccurancesOfString:@"[new-button]" withImage:[UIImage imageNamed:@"MyNewThingButtonImage"] scale:0];
labelWithImage.attributedText = stringWithImage;

Implementation:

@interface NSMutableAttributedString (InlineImage)

- (void)replaceCharactersInRange:(NSRange)range withInlineImage:(UIImage *)inlineImage scale:(CGFloat)inlineImageScale;

@end

@interface NSAttributedString (InlineImages)

- (NSAttributedString *)attributedStringByReplacingOccurancesOfString:(NSString *)string withInlineImage:(UIImage *)inlineImage scale:(CGFloat)inlineImageScale;

@end

.

@implementation NSMutableAttributedString (InlineImages)

- (void)replaceCharactersInRange:(NSRange)range withInlineImage:(UIImage *)inlineImage scale:(CGFloat)inlineImageScale {

    if (floorf(inlineImageScale) == 0)
        inlineImageScale = 1.0f;

    // Create resized, tinted image matching font size and (text) color
    UIImage *imageMatchingFont = [inlineImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    {
        // Font size
        NSDictionary *attributesForRange = [self attributesAtIndex:range.location effectiveRange:nil];
        UIFont *fontForRange = [attributesForRange valueForKey:NSFontAttributeName];
        CGSize imageSizeMatchingFontSize = CGSizeMake(inlineImage.size.width * (fontForRange.capHeight / inlineImage.size.height), fontForRange.capHeight);

        // Some scaling for prettiness
        CGFloat defaultScale = 1.4f;
        imageSizeMatchingFontSize = CGSizeMake(imageSizeMatchingFontSize.width * defaultScale,     imageSizeMatchingFontSize.height * defaultScale);
        imageSizeMatchingFontSize = CGSizeMake(imageSizeMatchingFontSize.width * inlineImageScale, imageSizeMatchingFontSize.height * inlineImageScale);
        imageSizeMatchingFontSize = CGSizeMake(ceilf(imageSizeMatchingFontSize.width), ceilf(imageSizeMatchingFontSize.height));

        // Text color
        UIColor *textColorForRange = [attributesForRange valueForKey:NSForegroundColorAttributeName];

        // Make the matching image
        UIGraphicsBeginImageContextWithOptions(imageSizeMatchingFontSize, NO, 0.0f);
        [textColorForRange set];
        [inlineImage drawInRect:CGRectMake(0 , 0, imageSizeMatchingFontSize.width, imageSizeMatchingFontSize.height)];
        imageMatchingFont = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    }

    // Text attachment with image
    NSTextAttachment *textAttachment = [NSTextAttachment new];
    textAttachment.image = imageMatchingFont;
    NSAttributedString *imageString = [NSAttributedString attributedStringWithAttachment:textAttachment];

    [self replaceCharactersInRange:range withAttributedString:imageString];
}

@end

@implementation NSAttributedString (InlineImages)

- (NSAttributedString *)attributedStringByReplacingOccurancesOfString:(NSString *)string withInlineImage:(UIImage *)inlineImage scale:(CGFloat)inlineImageScale {

    NSMutableAttributedString *attributedStringWithImages = [self mutableCopy];

    [attributedStringWithImages.string enumerateOccurancesOfString:string usingBlock:^(NSRange substringRange, BOOL *stop) {
        [attributedStringWithImages replaceCharactersInRange:substringRange withInlineImage:inlineImage scale:inlineImageScale];

    }];

    return [attributedStringWithImages copy];
}

@end
Community
  • 1
  • 1
Stian Høiland
  • 3,545
  • 2
  • 25
  • 32
  • Changing the line `UIFont *fontForRange = [attributesForRange valueForKey:NSFontAttributeName];` to `UIFont *fontForRange = [attributesForRange valueForKey:NSFontAttributeName] ?: [UIFont fontWithName:@"HelveticaNeue" size:12.0];` should be better, because [attributesForRange valueForKey:NSFontAttributeName] might be nil if it was not set in the dictionary – Victor Kwok Oct 29 '18 at 11:17
5

Problem solution in simple example is enter image description here

let attachment = NSTextAttachment()
attachment.image = UIImage(named: "qrcode")

let iconString = NSAttributedString(attachment: attachment)
let firstString = NSMutableAttributedString(string: "scan the ")
let secondString = NSAttributedString(string: "QR code received on your phone.")

firstString.append(iconString)
firstString.append(secondString)

self.textLabel.attributedText = firstString
Artem Novichkov
  • 2,260
  • 2
  • 19
  • 34
Prabhat Kasera
  • 1,111
  • 11
  • 28