Get index of character in UILabel from tap
Asked Answered
P

3

5

I'd like to get the index for the tapped character on a UILabel. I've subclassed a UILabel. In my awakeFromNib() I have this:

    layoutManager = NSLayoutManager()
    textStorage = NSTextStorage(attributedString: self.attributedText)
    textStorage.addLayoutManager(layoutManager)

    textContainer = NSTextContainer(size: CGSizeMake(self.frame.size.width, self.frame.size.height))
    textContainer.lineFragmentPadding = 0
    textContainer.maximumNumberOfLines = self.numberOfLines
    textContainer.lineBreakMode = self.lineBreakMode
    textContainer.size = self.frame.size

    layoutManager.addTextContainer(textContainer)

It is working how I want it to for the first 5 characters of the label, as in I tap the first character and in my touchesEnded I get an index of 0:

var touchedCharacterIndex = layoutManager.characterIndexForPoint(touch.locationInView(self), inTextContainer: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

But when I tap anywhere past the first four characters of the UILabel, I get either 4 or 0, which is incorrect.

Thanks,

Update

From a suggestion in the comments, I've added this:

 func updateTextStorage() {
    if let attributedText = self.attributedText {
        textStorage = NSTextStorage(attributedString: attributedText)
    }
    textStorage?.addLayoutManager(layoutManager)

    textContainer = NSTextContainer(size: CGSizeMake(self.frame.size.width, self.frame.size.height))
    textContainer?.lineFragmentPadding = 7
    textContainer?.maximumNumberOfLines = self.numberOfLines
    textContainer?.lineBreakMode = self.lineBreakMode
    textContainer?.size = self.bounds.size

    layoutManager.addTextContainer(textContainer!)
    layoutManager.textStorage = textStorage
}

I call this in layoutSubviews(), awakFromNib and the setters of bounds, frame, attributedText and text.

And it is now giving me some weird indexes, like if text length is 21, tapping the first letter gives me 6.

Pic answered 23/6, 2015 at 16:22 Comment(4)
You may want to override layoutSubviews() and update textContainer's size there. The label's frame is likely to change after awakeFromNib().Cassiodorus
Dang, I tried that and it is now giving me some weird indexes, like if length is 21, tapping the first letter gives me 6. I'll post an update. ThanksPic
Similarly, if the other properties change, you need to update them.Cassiodorus
I get the same results.Pic
S
14

TLDR: You are getting some weird indexes because the string seen by your layoutManager is layed-out differently than the one you actually see on screen.


You are getting some weird indexes from:

- (NSUInteger)characterIndexForPoint:(CGPoint)point
    inTextContainer:(NSTextContainer *)container
    fractionOfDistanceBetweenInsertionPoints:(nullable CGFloat *)partialFraction;

because your own layoutManager have to be in sync with the internal layoutManager from the system UILabel. Which mean when you set a font, size, etc. on your label, the internal layoutManager knows about that because it's taken care by UILabel, but your layoutManager instance does not.

So the text layout "seen" by your layoutManager is in a different position than the actual text rendered by the label on screen (default font and size I bet).

The complicated part is to keep those properties in sync, what you can do is set those on your attributed text like:

[mutableAttributedString addAttribute:NSFontAttributeName 
                                value:self.font 
                                range:NSMakeRange(0, mutableAttributedString.string.length)];

And pass this attributed string to your NSTextStorage instance. This fixed all the issues for me.


INFO: To help you debug you can render the string "seen" by your layoutManager using this:

- (void)drawTextInRect:(CGRect)rect
{
    [super drawTextInRect:rect];
    [self.layoutManager drawGlyphsForGlyphRange:NSMakeRange(0, self.textStorage.length) atPoint:CGPointMake(0, 0)];
}

If you see 2 strings layed-out on top of each other with some glitch this is your problem right there. If you can only see one that should be because they are perfectly aligned and your are good!

Supination answered 12/12, 2015 at 9:37 Comment(4)
Really useful tip. Thank you!Disaffect
I am assigning the attributed string with the font to the NSTextStorage but I am still having issues. Drawing the layoutManager is creating glitches alsoBoondocks
So the key is that you cannot access the rendered NSAttributedString or NSTextStorage of a UILabel.Yeh
works like charm ,this answer should be accept !I have waste hours to fix the bug before I see this answer, because I haven't set font in the attributeString 's attribute. and the characterIndexForPoint get weird index for the labelLanguorous
E
2

I think the issue is stemming from the fact that NSTextContainer lays out text like a UITextView, which means that it will not be vertically centered like a UILabel.

If you run the following piece of code you can see where your glyphs are laying out.

var location = layoutManager.locationForGlyphAtIndex(0);

It should show that the glyphs are being laid out at the top of your textContainer.

So wherever you call

var point = tapGesture.locationInView(self);

will need to be adjusted to take the vertical shift of the UILabel into account.

Emersion answered 24/6, 2015 at 16:2 Comment(0)
D
2

I've been trying to find something that works for this. Here is what I was able to get to work. It pulls from a few other examples but I had to include the Paragraph style to make it line up exactly. Hope this can help someone else.

NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeZero];
NSTextStorage *textStorage;

NSMutableParagraphStyle *paragraphStyle = NSMutableParagraphStyle.new;
paragraphStyle.alignment = self.label.textAlignment;
// Configure textContainer
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = self.label.lineBreakMode;
textContainer.maximumNumberOfLines = self.label.numberOfLines;

CGPoint locationOfTouchInLabel = [tapGesture locationInView:tapGesture.view];
CGSize labelSize = tapGesture.view.bounds.size;
textContainer.size = self.label.bounds.size;

NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithAttributedString:self.noPoliciesDescriptionLabel.attributedText];
[attributedText addAttributes:@{NSFontAttributeName: self.label.font, NSParagraphStyleAttributeName: paragraphStyle} range:NSMakeRange(0,self.label.attributedText.string.length)];
textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedText];

// Configure layoutManager and textStorage
[layoutManager addTextContainer:textContainer];
[textStorage addLayoutManager:layoutManager];

CGRect textBoundingBox = [layoutManager usedRectForTextContainer:textContainer];
CGPoint textContainerOffset = CGPointMake((labelSize.width - textBoundingBox.size.width) * 0.5 - textBoundingBox.origin.x,
                                          (labelSize.height - textBoundingBox.size.height) * 0.5 - textBoundingBox.origin.y);

CGPoint locationOfTouchInTextContainer = CGPointMake(locationOfTouchInLabel.x - textContainerOffset.x,
                                                     locationOfTouchInLabel.y - textContainerOffset.y);

NSInteger indexOfCharacter = [layoutManager characterIndexForPoint:locationOfTouchInTextContainer
                                                        inTextContainer:textContainer
                               fractionOfDistanceBetweenInsertionPoints:nil];

NSRange range = NSMakeRange(indexOfCharacter, 1);
NSString *value = [self.label.text substringWithRange:range];
NSLog(@"%@, %zd, %zd" , value, range.location, range.length);
Diffraction answered 17/11, 2017 at 19:49 Comment(2)
This update helps, but unfortunately is not complete when the UILabel contains multiple font styles (e.g. an emphasised word in bold weight). For a solution that takes all range attributes of the UILabel into account, see my comment https://mcmap.net/q/99746/-create-tap-able-quot-links-quot-in-the-nsattributedstring-of-a-uilabelPelecypod
Yes it helps but it gives index of previous object. I am using AHTagsLabel library and when there are less tags it gives me wrong index. After adding you code it gives me index but it's of previous tags.Rhondarhondda

© 2022 - 2024 — McMap. All rights reserved.