检测 iOS 中 UITextView 中属性文本的点击

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19332283/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 02:18:39  来源:igfitidea点击:

Detecting taps on attributed text in a UITextView in iOS

iosobjective-cuitextviewtextkit

提问by tarmes

I have a UITextViewwhich 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 UITextViewcan detect taps on a URL and call back my delegate, but these aren't URLs.

我有一个UITextView显示NSAttributedString. 这个字符串包含我想要点击的单词,这样当它们被点击时我会被回调,以便我可以执行一个操作。我意识到UITextView可以检测 URL 上的点击并回调我的委托,但这些不是 URL。

It seems to me that with iOS 7 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.

在我看来,有了 iOS 7 和 TextKit 的强大功能,这现在应该是可能的,但是我找不到任何示例,也不知道从哪里开始。

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 notrequired.

需要注意的是iOS 6的兼容性不是必需的。

采纳答案by tarmes

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.

我只是想多帮助别人一点。继 Shmidt 的回答之后,可以完全按照我在原始问题中的要求进行操作。

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

1) 创建一个属性字符串,其中自定义属性应用于可点击的单词。例如。

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:

2) 创建一个 UITextView 来显示该字符串,并向其添加一个 UITapGestureRecognizer。然后处理水龙头:

- (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!

当你知道怎么做的时候就这么简单!

回答by Suragch

Detecting taps on attributed text with Swift

使用 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 UITextViewto your project.

将 a 添加UITextView到您的项目中。

Outlet

出口

Connect the UITextViewto the ViewControllerwith an outlet named textView.

使用名为 的插座将 连接UITextView到。ViewControllertextView

Custom attribute

自定义属性

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

我们将通过制作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.

注意:这一步在技术上是可选的,但如果你不这样做,你将需要编辑下一部分中的代码以使用标准属性,如NSAttributedString.Key.foregroundColor. 使用自定义属性的优点是您可以定义要在属性文本范围中存储的值。

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.

使用File > New > File... > iOS > Source > Swift File添加一个新的 swift 文件。你可以随心所欲地称呼它。我打电话给我的NSAttributedStringKey+CustomAttribute.swift

Paste in the following code:

粘贴以下代码:

import Foundation

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

Code

代码

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

将 ViewController.swift 中的代码替换为以下内容。请注意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 = [ 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:

现在,如果您点击“Swift”的“w”,您应该得到以下结果:

character index: 1
character at index: w
You 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.
  • 在这里,我使用了一个自定义属性,但它也可以很容易地NSAttributedString.Key.foregroundColor(文本颜色)具有UIColor.green.
  • 以前,文本视图无法编辑或选择,但在我更新的 Swift 4.2 答案中,无论是否选择这些视图,它似乎都能正常工作。

Further study

进一步研究

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

这个答案是基于这个问题的其他几个答案。除了这些,另见

回答by natenash203

This is a slightly modified version, building off of @tarmes answer. I couldn't get the valuevariable to return anything but nullwithout 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.

这是一个稍微修改过的版本,基于@tarmes 的答案。我无法让value变量返回任何东西,但null没有下面的调整。此外,我需要返回完整的属性字典以确定结果操作。我会把这个放在评论中,但似乎没有代表这样做。如果我违反了协议,请提前道歉。

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

具体的调整是使用textView.textStorage而不是textView.attributedText. 作为一个仍在学习的 iOS 程序员,我不太确定这是为什么,但也许其他人可以启发我们。

Specific modification in the tap handling method:

在tap处理方法中的具体修改:

    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

    }
}

回答by Aditya Mathur

Making custom link and doing what you want on the tap has become much easier with iOS 7. There is very good example at Ray Wenderlich

在 iOS 7 中制作自定义链接和做你想做的事情变得更加容易。Ray Wenderlich有一个很好的例子

回答by Shmidt

WWDC 2013 example:

WWDC 2013 示例

NSLayoutManager *layoutManager = textView.layoutManager;
 CGPoint location = [touch locationInView:textView];
 NSUInteger characterIndex;
 characterIndex = [layoutManager characterIndexForPoint:location
inTextContainer:textView.textContainer
fractionOfDistanceBetweenInsertionPoints:NULL];
if (characterIndex < textView.textStorage.length) { 
// valid index
// Find the word range here
// using -enumerateSubstringsInRange:options:usingBlock:
}

回答by Jase Whatson

I was able to solve this pretty simply with NSLinkAttributeName

我能够用 NSLinkAttributeName 很简单地解决这个问题

Swift 2

斯威夫特 2

class MyClass: UIViewController, UITextViewDelegate {

  @IBOutlet weak var tvBottom: UITextView!

  override func viewDidLoad() {
      super.viewDidLoad()

     let attributedString = NSMutableAttributedString(string: "click me ok?")
     attributedString.addAttribute(NSLinkAttributeName, value: "cs://moreinfo", range: NSMakeRange(0, 5))
     tvBottom.attributedText = attributedString
     tvBottom.delegate = self

  }

  func textView(textView: UITextView, shouldInteractWithURL URL: NSURL, inRange characterRange: NSRange) -> Bool {
      UtilityFunctions.alert("clicked", message: "clicked")
      return false
  }

}

回答by Akila Wasala

Complete example for detect actions on attributed text with Swift 3

使用 Swift 3 检测属性文本上的操作的完整示例

let termsAndConditionsURL = TERMS_CONDITIONS_URL;
let privacyURL            = PRIVACY_URL;

override func viewDidLoad() {
    super.viewDidLoad()

    self.txtView.delegate = self
    let str = "By continuing, you accept the Terms of use and Privacy policy"
    let attributedString = NSMutableAttributedString(string: str)
    var foundRange = attributedString.mutableString.range(of: "Terms of use") //mention the parts of the attributed text you want to tap and get an custom action
    attributedString.addAttribute(NSLinkAttributeName, value: termsAndConditionsURL, range: foundRange)
    foundRange = attributedString.mutableString.range(of: "Privacy policy")
    attributedString.addAttribute(NSLinkAttributeName, value: privacyURL, range: foundRange)
    txtView.attributedText = attributedString
}

And then you can catch the action with shouldInteractWith URLUITextViewDelegate delegate method.So make sure you have set the delegate properly.

然后您可以使用shouldInteractWith URLUITextViewDelegate 委托方法捕获操作。因此请确保您已正确设置委托。

func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange) -> Bool {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: "WebView") as! SKWebViewController

        if (URL.absoluteString == termsAndConditionsURL) {
            vc.strWebURL = TERMS_CONDITIONS_URL
            self.navigationController?.pushViewController(vc, animated: true)
        } else if (URL.absoluteString == privacyURL) {
            vc.strWebURL = PRIVACY_URL
            self.navigationController?.pushViewController(vc, animated: true)
        }
        return false
    }

Like wise you can perform any action according to your requirement.

同样,您可以根据自己的要求执行任何操作。

Cheers!!

干杯!!

回答by Arek Holko

It's possible to do that with characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints:. It'll work somewhat differently than you wanted - you'll have to test if a tapped character belongs to a magic word. But it shouldn't be complicated.

可以用characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints:. 它的工作方式与您想要的略有不同 - 您必须测试轻按的字符是否属于魔法词。但应该不会很复杂。

BTW I highly recommend watching Introducing Text Kitfrom WWDC 2013.

顺便说一句,我强烈建议您观看WWDC 2013 的Introducing Text Kit

回答by Imanou Petit

With Swift 5 and iOS 12, you can create a subclass of UITextViewand override point(inside:with:)with some TextKit implementation in order to make only some NSAttributedStringsin it tappable.

使用 Swift 5 和 iOS 12,您可以创建一个子类UITextViewpoint(inside:with:)使用一些 TextKit 实现覆盖,以便仅使其中的一些NSAttributedStrings可点击。



The following code shows how to create a UITextViewthat only reacts to taps on underlined NSAttributedStrings in it:

以下代码显示了如何创建一个UITextView只对点击其中带下划线的NSAttributedStrings做出反应的s:

InteractiveUnderlinedTextView.swift

InteractiveUnderlinedTextView.swift

import UIKit

class InteractiveUnderlinedTextView: UITextView {

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        configure()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }

    func configure() {
        isScrollEnabled = false
        isEditable = false
        isSelectable = false
        isUserInteractionEnabled = true
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let superBool = super.point(inside: point, with: event)

        let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
        guard characterIndex < textStorage.length else { return false }
        let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil)

        return superBool && attributes[NSAttributedString.Key.underlineStyle] != nil
    }

}

ViewController.swift

视图控制器.swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let linkTextView = InteractiveUnderlinedTextView()
        linkTextView.backgroundColor = .orange

        let mutableAttributedString = NSMutableAttributedString(string: "Some text\n\n")
        let attributes = [NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue]
        let underlinedAttributedString = NSAttributedString(string: "Some other text", attributes: attributes)
        mutableAttributedString.append(underlinedAttributedString)
        linkTextView.attributedText = mutableAttributedString

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(underlinedTextTapped))
        linkTextView.addGestureRecognizer(tapGesture)

        view.addSubview(linkTextView)
        linkTextView.translatesAutoresizingMaskIntoConstraints = false
        linkTextView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        linkTextView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        linkTextView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor).isActive = true

    }

    @objc func underlinedTextTapped(_ sender: UITapGestureRecognizer) {
        print("Hello")
    }

}

回答by Mol0ko

Use this extension for Swift:

将此扩展用于 Swift:

import UIKit

extension UITapGestureRecognizer {

    func didTapAttributedTextInTextView(textView: UITextView, inRange targetRange: NSRange) -> Bool {
        let layoutManager = textView.layoutManager
        let locationOfTouch = self.location(in: textView)
        let index = layoutManager.characterIndex(for: locationOfTouch, in: textView.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)

        return NSLocationInRange(index, targetRange)
    }
}

Add UITapGestureRecognizerto your text view with following selector:

UITapGestureRecognizer使用以下选择器添加到文本视图:

guard let text = textView.attributedText?.string else {
        return
}
let textToTap = "Tap me"
if let range = text.range(of: tapableText),
      tapGesture.didTapAttributedTextInTextView(textView: textTextView, inRange: NSRange(range, in: text)) {
                // Tap recognized
}