Getting the Word Touched in a Uilabel/Uitextview

Getting the word touched in a UILabel/UITextView

(There is a link to a sample code project in your linked post that does contain some useful sample code, but I will outline the process for you here too.)

In short, you are going to need to use Core Text, which is Apple's advanced C-based text handling framework that backs all sophisticated text layout in iOS and OS X.

The full code is going to be somewhat involved, but key methods you are going to want to look at are:

CTFramesetterCreateWithAttributedString() - use this, in conjunction with an NSAttributedString that you will get from your label's text - to create the framesetter

CTFramesetterCreateFrame() - use this to get a CTFrameRef for your text from the above framesetter. You will need to create a CGPathRef using your label bounds to do this.

CTFrameGetLines(), CTFrameGetLineOrigins() - use these to get CTLineRefs corresponding to the typeset lines, and the coordinates of the line origins, respectively, then use CTLineGetStringIndexForPosition() to find the character index at a touch location.

You can then use this character index (in the line's reference frame) to work backward and find the actual character/word/etc within your full string.

Don't forget that matters are complicated by a couple issues:

  1. If you use UILabel's native drawing you will have to take care to perfectly match your typesetting metrics, which can be cumbersome since most of the objects (e.g. CTFontRef) are not toll-free bridged with their UIKit counterparts. Implementing your own drawing may, sometimes, be easier, which will guarantee metric matching.

  2. Core Text uses an inverted coordinate system with respect to the normal iOS drawing system. If you are getting wacky results, and especially if you do your own drawing, this is something to take a look at.

Not the easiest task in the world, but far from impossible. Good luck!

UILabel touch and get the text where touched

I think you'll find what you're looking for in the documentation for UITextInputProtocol.

For some more high level information, check out Apple's Text, Web and Editing Guide, specifically in the section titled "A Guided Tour of a UITextInput Implementation". It discusses how you can create indexed positions in text, and ask touches what text position they've landed nearest.

Apple's references a sample projected called SimpleTextInput, but I can't seem to find it. I'll keep looking.

Selecting a word in a UITextView

Update

This got a lot easier for iOS 7, thanks to the addition of NSLayoutManager in CoreText. If you're dealing with a UITextView you can access the layout manager as a property of the view. In my case I wanted to stick with a UILabel, so you have to create a layout manager with the same size, i.e:

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:labelText];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];
CGRect bounds = label.bounds;
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:bounds.size];
[layoutManager addTextContainer:textContainer];

Now you just need to find the index of the character that was clicked, which is simple!

NSUInteger characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];

Which makes it trivial to find the word itself:

if (characterIndex < textStorage.length) {
[labelText.string enumerateSubstringsInRange:NSMakeRange(0, textStorage.length)
options:NSStringEnumerationByWords
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
if (NSLocationInRange(characterIndex, enclosingRange)) {
// Do your thing with the word, at range 'enclosingRange'
*stop = YES;
}
}];
}

Original Answer, which works for iOS < 7

Thanks to @JP Hribovsek for some tips getting this working, I managed to solve this well enough for my purposes. It feels a little hacky, and likely wouldn't work too well for large bodies of text, but for paragraphs at a time (which is what I need) it's fine.

I created a simple UILabel subclass that allows me to set the inset value:

#import "WWLabel.h"

#define WWLabelDefaultInset 5

@implementation WWLabel

@synthesize topInset, leftInset, bottomInset, rightInset;

- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.topInset = WWLabelDefaultInset;
self.bottomInset = WWLabelDefaultInset;
self.rightInset = WWLabelDefaultInset;
self.leftInset = WWLabelDefaultInset;
}
return self;
}

- (void)drawTextInRect:(CGRect)rect
{
UIEdgeInsets insets = {self.topInset, self.leftInset,
self.bottomInset, self.rightInset};

return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];
}

Then I created a UIView subclass that contained my custom label, and on tap constructed the size of the text for each word in the label, until the size exceeded that of the tap location - this is the word that was tapped. It's not prefect, but works well enough for now.

I then used a simple NSAttributedString to highlight the text:

#import "WWPhoneticTextView.h"
#import "WWLabel.h"

#define WWPhoneticTextViewInset 5
#define WWPhoneticTextViewDefaultColor [UIColor blackColor]
#define WWPhoneticTextViewHighlightColor [UIColor yellowColor]

#define UILabelMagicTopMargin 5
#define UILabelMagicLeftMargin -5

@implementation WWPhoneticTextView {
WWLabel *label;
NSMutableAttributedString *labelText;
NSRange tappedRange;
}

// ... skipped init methods, very simple, just call through to configureView

- (void)configureView
{
if(!label) {
tappedRange.location = NSNotFound;
tappedRange.length = 0;

label = [[WWLabel alloc] initWithFrame:[self bounds]];
[label setLineBreakMode:NSLineBreakByWordWrapping];
[label setNumberOfLines:0];
[label setBackgroundColor:[UIColor clearColor]];
[label setTopInset:WWPhoneticTextViewInset];
[label setLeftInset:WWPhoneticTextViewInset];
[label setBottomInset:WWPhoneticTextViewInset];
[label setRightInset:WWPhoneticTextViewInset];

[self addSubview:label];
}


// Setup tap handling
UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleSingleTap:)];
singleFingerTap.numberOfTapsRequired = 1;
[self addGestureRecognizer:singleFingerTap];
}

- (void)setText:(NSString *)text
{
labelText = [[NSMutableAttributedString alloc] initWithString:text];
[label setAttributedText:labelText];
}

- (void)handleSingleTap:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
// Get the location of the tap, and normalise for the text view (no margins)
CGPoint tapPoint = [sender locationInView:sender.view];
tapPoint.x = tapPoint.x - WWPhoneticTextViewInset - UILabelMagicLeftMargin;
tapPoint.y = tapPoint.y - WWPhoneticTextViewInset - UILabelMagicTopMargin;

// Iterate over each word, and check if the word contains the tap point in the correct line
__block NSString *partialString = @"";
__block NSString *lineString = @"";
__block int currentLineHeight = label.font.pointSize;
[label.text enumerateSubstringsInRange:NSMakeRange(0, [label.text length]) options:NSStringEnumerationByWords usingBlock:^(NSString* word, NSRange wordRange, NSRange enclosingRange, BOOL* stop){

CGSize sizeForText = CGSizeMake(label.frame.size.width-2*WWPhoneticTextViewInset, label.frame.size.height-2*WWPhoneticTextViewInset);
partialString = [NSString stringWithFormat:@"%@ %@", partialString, word];

// Find the size of the partial string, and stop if we've hit the word
CGSize partialStringSize = [partialString sizeWithFont:label.font constrainedToSize:sizeForText lineBreakMode:label.lineBreakMode];

if (partialStringSize.height > currentLineHeight) {
// Text wrapped to new line
currentLineHeight = partialStringSize.height;
lineString = @"";
}
lineString = [NSString stringWithFormat:@"%@ %@", lineString, word];

CGSize lineStringSize = [lineString sizeWithFont:label.font constrainedToSize:label.frame.size lineBreakMode:label.lineBreakMode];
lineStringSize.width = lineStringSize.width + WWPhoneticTextViewInset;

if (tapPoint.x < lineStringSize.width && tapPoint.y > (partialStringSize.height-label.font.pointSize) && tapPoint.y < partialStringSize.height) {
NSLog(@"Tapped word %@", word);
if (tappedRange.location != NSNotFound) {
[labelText addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:tappedRange];
}

tappedRange = wordRange;
[labelText addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:tappedRange];
[label setAttributedText:labelText];
*stop = YES;
}
}];
}
}

Tap Gesture on part of UILabel

You could try https://github.com/mattt/TTTAttributedLabel and add a link to the label. When the link is pressed you get a action, so part of the label click works only thing you have to would be customizing the link part of the label. I tried this in the past and it worked flawlessly but my client was not interested in using a third party component so duplicated this functionality using UIWebView and HTML.

How to extract sub-string from UILabel based on finger pressed location?

Once I press on this UILabel, I want to extract one word from the
location where my finger pressed.

That's not something that's going to be easy to do with UILabel. UILabel is meant to be a simple way to put static text on the screen; it doesn't provide features that would let you determine the frames of individual words.

You'll probably be better off creating your own view for this. You'll want to dig into Core Text to lay out and draw the text. Core Text is a lot more complicated than just using a simple UILabel, but it gives you the information and control you'll need to determine where each word is drawn on the screen. Your view can use that information to map touches to words.



Related Topics



Leave a reply



Submit