I\'ve UILabel which has some text and 2 month names like \"In the month of January and July, sun shine will be peak\" I sub classed UILabel and added touch event to it. Now
I handled this problem by putting transaparent UIViews on top of the UILabel which were wired to handle touch events. You can adjust the position and size of the hit zone labels by calculating the size of the relevant portions of text. This approach also has the advantage of being able to tweak the sizes of your hit zones to make them bigger and so easier to tap with fat fingers.
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.
- (void)tapped:(UITapGestureRecognizer *)tap
{
CGPoint location = [tap locationInView:_label];
NSDictionary *attribute = @{NSFontAttributeName: _label.font};
CGFloat width = 0;
for (int i = 0; i substringWithRange:NSMakeRange(i, 1)];
CGSize lettersize = [letterStr sizeWithAttributes:attribute];
width += lettersize.width;
if (width >= location.x ) {
NSLog(@"str: %@", letterStr);
break;
}
}
}
I had a similar problem as the original poster. I wanted the user to be able to long-press in a UILabel which was displaying a comma-separated list of strings, and extract a sub-string corresponding to the touch location between the closest enclosing commas.
The problem is that UILabel doesn't implement the UITextInput protocol, otherwise the implementation would be straightforward.
I didn't like any of the suggested workarounds I found, so I decided to come up with my own solution. After some thought and research, I realized that while UILabel doesn't support UITextInput, UITextView does. I didn't want to replace all the UILabels in the UI with UITextViews, so I thought, "why not programmatically create a UITextView behind the scenes, with the exact same text rendering settings, and use its UITextInput methods to map the touch point to the desired sub-string?".
This proved to be the ideal solution from my point of view.
I attached a UILongPressGestureRecognizer to the UILabels and call the following function in the gesture handler:
-(NSString*)commaDelineatedTextAtTouchPosition:(UILongPressGestureRecognizer*)longPress
{
UILabel *gestureRecipient = (UILabel*)longPress.view;
UITextView *textRegionClone = [[UITextView alloc] initWithFrame:gestureRecipient.frame];
// tweak the text view's content area to get the same rendering (placement and wrapping)
textRegionClone.textContainerInset = UIEdgeInsetsMake(0.0, -5.0, 0.0, -5.0);
// copy the label's text properties
textRegionClone.text = gestureRecipient.text;
textRegionClone.font = gestureRecipient.font;
[textRegionClone setNeedsDisplay];
CGPoint loc = [longPress locationInView:gestureRecipient];
UITextPosition *charR = [textRegionClone closestPositionToPoint:loc];
id<UITextInputTokenizer> tokenizer = textRegionClone.tokenizer;
UITextRange *searchRange = [tokenizer rangeEnclosingPosition:charR withGranularity:UITextGranularityCharacter inDirection:UITextStorageDirectionBackward];
NSString *commaEnclosedText = @"";
if (searchRange != nil) {
NSString *tapChar = [textRegionClone textInRange:searchRange];
if ([tapChar isEqualToString:@","]) { // tapped right on a ","
// move the end of the range to immediately before the ","
searchRange = [textRegionClone textRangeFromPosition:searchRange.start toPosition:[textRegionClone positionFromPosition:searchRange.end offset:-1]];
}
UITextPosition *docStart = textRegionClone.beginningOfDocument;
// search back to find the leading comma or the beginning of the text
do {
searchRange = [textRegionClone textRangeFromPosition:[textRegionClone positionFromPosition:searchRange.start offset:-1] toPosition:searchRange.end];
commaEnclosedText = [textRegionClone textInRange:searchRange];
}
while (([searchRange.start isEqual:docStart] == NO) && ([commaEnclosedText characterAtIndex:0] != ','));
// now search forward to the trailing comma or the end of the text
UITextPosition *docEnd = textRegionClone.endOfDocument;
while (([searchRange.end isEqual:docEnd] == NO) && ([commaEnclosedText characterAtIndex:commaEnclosedText.length - 1] != ',')) {
searchRange = [textRegionClone textRangeFromPosition:searchRange.start toPosition:[textRegionClone positionFromPosition:searchRange.end offset:1]];
commaEnclosedText = [textRegionClone textInRange:searchRange];
}
commaEnclosedText = [[commaEnclosedText stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@","]] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
return commaEnclosedText;
}
The only thing that's a little bit hacky about this technique is that you have to hand-tweak the rendering in the UITextView by adjusting the textContainerInset region. Otherwise you don't get exactly the same wrapping and placement of the text in the UITextView as you do in the UILabel. Otherwise, this is reasonably generic code that should work for anyone else.
I've implemented your question, just for fun on my blog.
Simply i've subclassed UILabel
adding touchEnded
, to recognize touch position and letter position.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[touches allObjects] objectAtIndex:0];
CGPoint pos = [touch locationInView:self];
int sizes[self.text.length];
for ( int i=0; i<self.text.length; i++ )
{
char letter = [self.text characterAtIndex:i];
NSString *letterStr = [NSString stringWithFormat:@"%c", letter];
CGSize letterSize = [letterStr sizeWithFont:self.font];
sizes[i] = letterSize.width;
}
int sum = 0;
for ( int i=0; i<self.text.length; i++)
{
sum += sizes[i];
if ( sum >= pos.x )
{
[ _delegate didLetterFound:[ self.text characterAtIndex:i] ];
return;
}
}
}
Hope this helps
cheers.