Detecting taps on attributed text in a UITextView in iOS

I have a UITextView which displays an NSAttributedString. This string contains words that I'd like to make tappable, such that when they are tapped I get called back so that I can perform an action. I realise that UITextView can detect taps on a URL and call back my delegate, but these aren't URLs.

It seems to me that with iOS7 and the power of TextKit this should now be possible, however I can't find any examples and I'm not sure where to start.

I understand that it's now possible to create custom attributes in the string (although I haven't done this yet), and perhaps these will be useful to detecting if one of the magic words has been tapped? In any case, I still don't know how to intercept that tap and detect on which word the tap occurred.

Note that iOS 6 compatibility is not required.


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!


Updated for Swift 3

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 and uses Swift 3.

Add a UITextView to your project.

在这里输入图像描述

Settings

Use the following settings in the Attributes inspector:

Outlet

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

Code

Add code to your View Controller to detect the tap. Note the UIGestureRecognizerDelegate .

import UIKit
class 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 = [ "MyCustomAttributeName": "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)
    }

    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 = "MyCustomAttributeName"
            let attributeValue = myTextView.attributedText.attribute(attributeName, at: characterIndex, effectiveRange: nil) as? String
            if let value = attributeValue {
                print("You tapped on (attributeName) and the value is: (value)")
            }

        }
    }
}

在这里输入图像描述

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

在这里输入图像描述

Notes

  • Here I used a custom attribute, but it could have just as easily been NSForegroundColorAttributeName (text color) that has a value of UIColor.greenColor() .
  • This only works if the text view is set as not editable and not selectable, as described in the Settings section above. Making it editable and selectable is the reason for the problem discussed in the comments below.
  • Further study

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

  • Advanced Text Layouts and Effects with Text Kit (WWDC 2013 video)
  • Attributed String Programming Guide
  • How do I make an attributed string using Swift?

  • This is a slightly modified version, building off of @tarmes answer. I couldn't get the value variable 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
    
        }
    }
    
    链接地址: http://www.djcxy.com/p/30674.html

    上一篇: 哪个对象被挖掘?

    下一篇: 在iOS中检测UITextView中的属性文本的点击