For a UILabel
, I'd like to find out which character index is at specific point received from a touch event. I'd like to solve this problem for iOS 7 using Text Kit.
Since UILabel doesn't provide access to its NSLayoutManager
, I created my own based on UILabel
's configuration like this:
- (void)textTapped:(UITapGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateEnded) {
CGPoint location = [recognizer locationInView:self];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:self.attributedText];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.bounds.size];
[layoutManager addTextContainer:textContainer];
textContainer.maximumNumberOfLines = self.numberOfLines;
textContainer.lineBreakMode = self.lineBreakMode;
NSUInteger characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textStorage.length) {
NSRange range = NSMakeRange(characterIndex, 1);
NSString *value = [self.text substringWithRange:range];
NSLog(@"%@, %zd, %zd", value, range.location, range.length);
}
}
}
The code above is in a UILabel
subclass with a UITapGestureRecognizer
configured to call textTapped:
(Gist).
The resulting character index makes sense (increases when tapping from left to right), but is not correct (the last character is reached at roughly half the width of the label). It looks like maybe the font size or text container size is not configured properly, but can't find the problem.
I'd really like to keep my class a subclass of UILabel
instead of using UITextView
. Has anyone solved this problem for UILabel
?
Update: I spent a DTS ticket on this question and the Apple engineer recommended to override UILabel
's drawTextInRect:
with an implementation that uses my own layout manager, similar to this code snippet:
- (void)drawTextInRect:(CGRect)rect
{
[yourLayoutManager drawGlyphsForGlyphRange:NSMakeRange(0, yourTextStorage.length) atPoint:CGPointMake(0, 0)];
}
I think it would be a lot of work to keep my own layout manager in sync with the label's settings, so I'll probably go with UITextView
despite my preference for UILabel
.
Update 2: I decided to use UITextView
after all. The purpose of all this was to detect taps on links embedded in the text. I tried to use NSLinkAttributeName
, but this setup didn't trigger the delegate callback when tapping a link quickly. Instead, you have to press the link for a certain amount of time – very annoying. So I created CCHLinkTextView that doesn't have this problem.
Here you are my implementation for the same problem. I have needed to mark
#hashtags
and@usernames
with reaction on the taps.I do not override
drawTextInRect:(CGRect)rect
because default method works perfect.Also I have found the following nice implementation https://github.com/Krelborn/KILabel. I used some ideas from this sample too.
i got the same error as you, the index increased way to fast so it wasn't accurate at the end. The cause of this issue was that
self.attributedText
did not contain full font information for the whole string.When UILabel renders it uses the font specified in
self.font
and applies it to the whole attributedString. This is not the case when assigning the attributedText to the textStorage. Therefore you need to do this yourself:Swift 4
Hope this helps :)
I have implemented the same on swift 3. Below is the complete code to find Character index at touch point for UILabel, it can help others who are working on swift and looking for the solution :
I played around with the solution of Alexey Ishkov. Finally i got a solution! Use this code snippet in your UITapGestureRecognizer selector:
Hope this will help some people out there!
Swift 4, synthesized from many sources including good answers here. My contribution is correct handling of inset, alignment, and multi-line labels. (most implementations treat a tap on trailing whitespace as a tap on the final character in the line)