Detecting taps on attributed text in a UITextView in iOS Detecting taps on attributed text in a UITextView in iOS ios ios

Detecting taps on attributed text in a UITextView in iOS


I just wanted to help others a little more. Following on from Shmidt's response it's possible to do exactly as I had asked in my original question.

1) Create an attributed string with custom attributes applied to the clickable words. eg.

NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a clickable word" attributes:@{ @"myCustomTag" : @(YES) }];[paragraph appendAttributedString:attributedString];

2) Create a UITextView to display that string, and add a UITapGestureRecognizer to it. Then handle the tap:

- (void)textTapped:(UITapGestureRecognizer *)recognizer{    UITextView *textView = (UITextView *)recognizer.view;    // Location of the tap in text-container coordinates    NSLayoutManager *layoutManager = textView.layoutManager;    CGPoint location = [recognizer locationInView:textView];    location.x -= textView.textContainerInset.left;    location.y -= textView.textContainerInset.top;    // Find the character that's been tapped on    NSUInteger characterIndex;    characterIndex = [layoutManager characterIndexForPoint:location                                           inTextContainer:textView.textContainer                  fractionOfDistanceBetweenInsertionPoints:NULL];    if (characterIndex < textView.textStorage.length) {        NSRange range;        id value = [textView.attributedText attribute:@"myCustomTag" atIndex:characterIndex effectiveRange:&range];        // Handle as required...        NSLog(@"%@, %d, %d", value, range.location, range.length);    }}

So easy when you know how!


Detecting taps on attributed text with Swift

Sometimes for beginners it is a little hard to know how to do get things set up (it was for me anyway), so this example is a little fuller.

Add a UITextView to your project.

Outlet

Connect the UITextView to the ViewController with an outlet named textView.

Custom attribute

We are going to make a custom attribute by making an Extension.

Note: This step is technically optional, but if you don't do it you will need to edit the code in the next part to use a standard attribute like NSAttributedString.Key.foregroundColor. The advantage of using a custom attribute is that you can define what values you want to store in the attributed text range.

Add a new swift file with File > New > File... > iOS > Source > Swift File. You can call it what you want. I am calling mine NSAttributedStringKey+CustomAttribute.swift.

Paste in the following code:

import Foundationextension NSAttributedString.Key {    static let myAttributeName = NSAttributedString.Key(rawValue: "MyCustomAttribute")}

Code

Replace the code in ViewController.swift with the following. Note the UIGestureRecognizerDelegate.

import UIKitclass ViewController: UIViewController, UIGestureRecognizerDelegate {    @IBOutlet weak var textView: UITextView!    override func viewDidLoad() {        super.viewDidLoad()        // Create an attributed string        let myString = NSMutableAttributedString(string: "Swift attributed text")        // Set an attribute on part of the string        let myRange = NSRange(location: 0, length: 5) // range of "Swift"        let myCustomAttribute = [ NSAttributedString.Key.myAttributeName: "some value"]        myString.addAttributes(myCustomAttribute, range: myRange)        textView.attributedText = myString        // Add tap gesture recognizer to Text View        let tap = UITapGestureRecognizer(target: self, action: #selector(myMethodToHandleTap(_:)))        tap.delegate = self        textView.addGestureRecognizer(tap)    }    @objc func myMethodToHandleTap(_ sender: UITapGestureRecognizer) {        let myTextView = sender.view as! UITextView        let layoutManager = myTextView.layoutManager        // location of tap in myTextView coordinates and taking the inset into account        var location = sender.location(in: myTextView)        location.x -= myTextView.textContainerInset.left;        location.y -= myTextView.textContainerInset.top;        // character index at tap location        let characterIndex = layoutManager.characterIndex(for: location, in: myTextView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)        // if index is valid then do something.        if characterIndex < myTextView.textStorage.length {            // print the character index            print("character index: \(characterIndex)")            // print the character at the index            let myRange = NSRange(location: characterIndex, length: 1)            let substring = (myTextView.attributedText.string as NSString).substring(with: myRange)            print("character at index: \(substring)")            // check if the tap location has a certain attribute            let attributeName = NSAttributedString.Key.myAttributeName            let attributeValue = myTextView.attributedText?.attribute(attributeName, at: characterIndex, effectiveRange: nil)            if let value = attributeValue {                print("You tapped on \(attributeName.rawValue) and the value is: \(value)")            }        }    }}

enter image description here

Now if you tap on the "w" of "Swift", you should get the following result:

character index: 1character at index: wYou tapped on MyCustomAttribute and the value is: some value

Notes

  • Here I used a custom attribute, but it could have just as easily been NSAttributedString.Key.foregroundColor (text color) that has a value of UIColor.green.
  • Formerly the text view could not be editable or selectable, but in my updated answer for Swift 4.2 it seems to be working fine no matter whether these are selected or not.

Further study

This answer was based on several other answers to this question. Besides these, see also


This is a slightly modified version, building off of @tarmes answer. I couldn't get the valuevariable to return anything but null without the tweak below. Also, I needed the full attribute dictionary returned in order to determine the resulting action. I would have put this in the comments but don't appear to have the rep to do so. Apologies in advance if I have violated protocol.

Specific tweak is to use textView.textStorage instead of textView.attributedText. As a still learning iOS programmer, I am not really sure why this is, but perhaps someone else can enlighten us.

Specific modification in the tap handling method:

    NSDictionary *attributesOfTappedText = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];

Full code in my view controller

- (void)viewDidLoad{    [super viewDidLoad];    self.textView.attributedText = [self attributedTextViewString];    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(textTapped:)];    [self.textView addGestureRecognizer:tap];}  - (NSAttributedString *)attributedTextViewString{    NSMutableAttributedString *paragraph = [[NSMutableAttributedString alloc] initWithString:@"This is a string with " attributes:@{NSForegroundColorAttributeName:[UIColor blueColor]}];    NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a tappable string"                                                                       attributes:@{@"tappable":@(YES),                                                                                    @"networkCallRequired": @(YES),                                                                                    @"loadCatPicture": @(NO)}];    NSAttributedString* anotherAttributedString = [[NSAttributedString alloc] initWithString:@" and another tappable string"                                                                              attributes:@{@"tappable":@(YES),                                                                                           @"networkCallRequired": @(NO),                                                                                           @"loadCatPicture": @(YES)}];    [paragraph appendAttributedString:attributedString];    [paragraph appendAttributedString:anotherAttributedString];    return [paragraph copy];}- (void)textTapped:(UITapGestureRecognizer *)recognizer{    UITextView *textView = (UITextView *)recognizer.view;    // Location of the tap in text-container coordinates    NSLayoutManager *layoutManager = textView.layoutManager;    CGPoint location = [recognizer locationInView:textView];    location.x -= textView.textContainerInset.left;    location.y -= textView.textContainerInset.top;    NSLog(@"location: %@", NSStringFromCGPoint(location));    // Find the character that's been tapped on    NSUInteger characterIndex;    characterIndex = [layoutManager characterIndexForPoint:location                                       inTextContainer:textView.textContainer              fractionOfDistanceBetweenInsertionPoints:NULL];    if (characterIndex < textView.textStorage.length) {        NSRange range;        NSDictionary *attributes = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];        NSLog(@"%@, %@", attributes, NSStringFromRange(range));        //Based on the attributes, do something        ///if ([attributes objectForKey:...)] //make a network call, load a cat Pic, etc    }}