Get index of character in UILabel from tap

I would like to get the index for the involved symbol on the UILabel. I subclassed 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 works the way I want it to be for the first 5 characters of the label, since I touched the first character and in my contacts. I got index 0:

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

But when I click somewhere past the first four characters of the UILabel, I get either 4 or 0, which is not correct.

Thank,

Update

From a suggestion in the comments, I added the following:

 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 it in layoutSubviews()

, awakFromNib

and setters bounds

, frame

, attributedText

and text

.

And now it gives me some weird indices, like if the text length is 21, pressing the first letter gives me 6.

+3


source to share


3 answers


TL; DR: You get some weird indexes because the line yours layoutManager

see is exposed differently than the one you actually see on screen.


You get some weird indexes:

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

      

because your own layoutManager

needs to be in sync with the internal one layoutManager

from the system UILabel

. This means that when you set the font, size, etc. On its own label, the inner layoutManager

knows about it because it took care of it UILabel

, but your instance layoutManager

doesn't.

So the layout of the text as seen by your layoutManager is in a different position than the actual text displayed by the label on the screen (default font size and size).

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



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

      

And pass this attribute string to your instance NSTextStorage

. This fixed all problems for me.


INFO: to help you debug, you can display the line "seen" by yours layoutManager

using the following command:

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

      

If you see two lines stacked on top of each other with some glitches, that's your problem right there. If you can only see the one that should be, because they are perfectly aligned and you are good!

+3


source


I think the problem is with the NSTextContainer outputting the text as a UITextView, which means it will not be vertically centered like a UILabel.

If you run the following piece of code, you can see where the glyphs are laid out.

var location = layoutManager.locationForGlyphAtIndex(0);

      

It should show that the glyphs are laid out at the top of your text container.



So when you call

var point = tapGesture.locationInView(self);

      

needs to be adjusted to accommodate the vertical displacement of the UILabel.

+1


source


I am trying to find something that works for this. Here's what I was able to get started. This draws from several other examples, but I had to include the paragraph style to make it match exactly. Hope this helps 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);

      

+1


source







All Articles