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.
I played around with the solution of Alexey Ishkov. Finally i got a solution! Use this code snippet in your UITapGestureRecognizer selector:
UILabel *textLabel = (UILabel *)recognizer.view;
CGPoint tapLocation = [recognizer locationInView:textLabel];
// init text storage
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:textLabel.attributedText];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
// init text container
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(textLabel.frame.size.width, textLabel.frame.size.height+100) ];
textContainer.lineFragmentPadding = 0;
textContainer.maximumNumberOfLines = textLabel.numberOfLines;
textContainer.lineBreakMode = textLabel.lineBreakMode;
[layoutManager addTextContainer:textContainer];
NSUInteger characterIndex = [layoutManager characterIndexForPoint:tapLocation
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
Hope this will help some people out there!