I finally found a reliable way to detect touches on specific parts of a UILabel
that has NSAttributedString
content on its .attributedText
property.
(UPDATE: Sadly, I discovered that this technique only works on one line labels, or multi-line labels with their .textAlignment
property set to NSTextAlignmentLeft
.)
In short, remember that you can assign any key-value pair as an attribute on a NSAttributedString
. So, by assigning custom attributes, you can have multiple ‘callbacks’ associated with taps on that content type. Imagine a label that has ‘terms of service’ and ‘privacy policy’. You can give them those specific attributes.
Anyway, here’s a recipe that should get you started. It’s quick and dirty, and the snippet below would be for example in a UIViewController
subclass:
static NSString * const MyCustomAttribute = @"CustomAttribute"; // NSNumber value (BOOL) - (void)configureAttributedTextLabel { NSString *linkText = @"Stephen"; self.attributedTextLabel.text = [NSString stringWithFormat: @"Here we have some super long string that will span multiple lines, just to check. Isn't that right %@\?", linkText]; NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:self.attributedTextLabel.text]; NSRange userNameRange = [self.attributedTextLabel.text rangeOfString:linkText]; NSRange notUserNameRange = (NSRange){0, userNameRange.location}; [result setAttributes:@{NSForegroundColorAttributeName: self.attributedTextLabel.textColor, NSFontAttributeName: self.attributedTextLabel.font} range:notUserNameRange]; [result setAttributes:@{NSForegroundColorAttributeName: [QLAppearance primaryColor], NSFontAttributeName: self.attributedTextLabel.font, NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle), MyCustomAttribute : @YES} range:userNameRange]; self.attributedTextLabel.attributedText = result; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tappedLabel:)]; self.attributedTextLabel.userInteractionEnabled = YES; [self.attributedTextLabel addGestureRecognizer:tap]; } - (void)tappedLabel:(UITapGestureRecognizer*)tap { UILabel *label = (UILabel*)tap.view; if (!label.attributedText) { return; } NSTextStorage *storage = [[NSTextStorage alloc] initWithAttributedString:label.attributedText]; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:label.bounds.size]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [layoutManager addTextContainer:textContainer]; [storage addLayoutManager:layoutManager]; textContainer.lineFragmentPadding = 0.0; textContainer.lineBreakMode = label.lineBreakMode; textContainer.maximumNumberOfLines = label.numberOfLines; CGPoint location = [tap locationInView:label]; NSUInteger characterIndex = [layoutManager characterIndexForPoint:location inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:NULL]; if (characterIndex < storage.length) { NSLog(@"Character Index: %i", (int)characterIndex); NSRange range = NSMakeRange(characterIndex, 1); NSString *substring = [label.attributedText.string substringWithRange:range]; NSLog(@"Character at Index: %@", substring); NSString *attributeName = MyCustomAttribute; NSNumber *attributeValue = [label.attributedText attribute:attributeName atIndex:characterIndex effectiveRange:nil]; if (attributeValue) { NSLog(@"You tapped on %@ and the value is: %@", attributeName, NSStringFromBOOL(attributeValue.boolValue)); // use this for further callbacks! } } }
And that’s pretty much it!