【问题标题】:Detecting taps on attributed text in a UITextView in iOS在 iOS 的 UITextView 中检测属性文本的点击
【发布时间】:2013-10-20 08:50:51
【问题描述】:

我有一个UITextView,它显示一个NSAttributedString。该字符串包含我希望使其可点击的单词,这样当他们被点击时,我会被回调,以便我可以执行操作。我意识到UITextView 可以检测到对 URL 的点击并回调我的代理,但这些不是 URL。

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

我知道现在可以在字符串中创建自定义属性(尽管我还没有这样做),也许这些对于检测是否已点击某个魔术词很有用?无论如何,我仍然不知道如何拦截该点击并检测点击发生在哪个单词上。

请注意,不需要兼容 iOS 6。

【问题讨论】:

  • 注意:在 iOS 10 及更高版本中,请改用 NSAttributedString.Key.link 属性。请参阅我的答案 - 但是,在此之前,您似乎必须在这里接受接受的答案。

标签: ios objective-c uitextview textkit


【解决方案1】:

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

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

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

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);

    }
}

知道怎么做就这么简单!

【讨论】:

  • 你会如何在 IOS 6 中解决这个问题?你能看看这个问题吗?stackoverflow.com/questions/19837522/…
  • 实际上 characterIndexForPoint:inTextContainer: fractionOfDistanceBetweenInsertionPoints 在 iOS 6 上可用,所以我认为它应该可以工作。让我们知道!以这个项目为例:github.com/laevandus/NSTextFieldHyperlinks/blob/master/…
  • 文档说它只在 IOS 7 或更高版本中可用 :)
  • 是的,对不起。我让自己对 Mac OS 感到困惑!这仅适用于 iOS7。
  • 当你有不可选择的 UITextView 时,它似乎不起作用
【解决方案2】:

使用 Swift 检测属性文本的点击

有时对于初学者来说,知道如何进行设置有点困难(反正对我来说是这样),所以这个例子更完整一些。

UITextView 添加到您的项目中。

出口

使用名为textView 的插座将UITextView 连接到ViewController

自定义属性

我们将通过创建Extension 来创建自定义属性。

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

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

粘贴以下代码:

import Foundation

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

代码

将 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)")
            }

        }
    }
}

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

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

注意事项

  • 这里我使用了一个自定义属性,但它也可以很容易地使用值为UIColor.greenNSAttributedString.Key.foregroundColor(文本颜色)。
  • 以前文本视图无法编辑或选择,但在我对 Swift 4.2 的更新答案中,无论是否选择这些似乎都可以正常工作。

进一步研究

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

【讨论】:

  • 使用myTextView.textStorage 而不是myTextView.attributedText.string
  • 在 iOS 9 中通过点击手势检测点击不适用于连续点击。有什么更新吗?
  • @WaqasMahmood,我为这个问题开始了a new question。您可以为它加注星标,稍后再回来查看任何答案。如果有更多相关细节,请随意编辑该问题或添加 cmets。
  • @dejix 我通过每次在我的 TextView 末尾添加另一个“”空字符串来解决问题。这样,检测会在您最后一句话之后停止。希望对你有帮助
  • 多次点击都能完美运行,我只是输入了一个简短的例程来证明这一点: if characterIndex
【解决方案3】:

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

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

点击处理方式的具体修改:

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

我的视图控制器中的完整代码

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

    }
}

【讨论】:

  • textView.attributedText 也有同样的问题!感谢您的 textView.textStorage 提示!
  • 在 iOS 9 中通过点击手势检测点击不适用于连续点击。
【解决方案4】:

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

【讨论】:

  • 这比尝试计算相对于其容器视图的字符串位置要干净得多。
  • 问题是 textView 需要是可选择的,我不希望这种行为。
  • @ThomásC。 +1 表示为什么我的UITextView 没有检测到链接,即使我已将其设置为通过 IB 检测它们。 (我也让它无法选择)
【解决方案5】:

WWDC 2013 example:

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:
}

【讨论】:

  • 谢谢!我也会看 WWDC 视频。
  • @Suragch “带有文本工具包的高级文本布局和效果”。
【解决方案6】:

我可以很简单地用 NSLinkAttributeName 解决这个问题

斯威夫特 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
  }

}

【讨论】:

  • 您应该检查您的 URL 是否被点击,而不是另一个带有 if URL.scheme == "cs"return trueif 语句之外的 URL,以便 UITextView 可以处理被点击的正常 https:// 链接
  • 我这样做了,它在 iPhone 6 和 6+ 上运行得相当好,但在 iPhone 5 上根本不工作。使用上面的 Suragch 解决方案,它只是工作。从来不知道为什么 iPhone 5 会出现这个问题,这毫无意义。
【解决方案7】:

使用 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
}

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

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
    }

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

干杯!!

【讨论】:

  • 谢谢!你拯救了我的一天!
【解决方案8】:

characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints: 可以做到这一点。它的工作方式与您想要的有所不同 - 您必须测试点击的字符是否属于 魔术词。但它不应该很复杂。

顺便说一句,我强烈推荐观看 WWDC 2013 的 Introducing Text Kit

【讨论】:

    【解决方案9】:

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


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

    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

    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")
        }
    
    }
    

    【讨论】:

    • 嗨,有什么办法可以使它符合多个属性而不是一个?
    【解决方案10】:

    将此扩展用于 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)
        }
    }
    

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

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

    【讨论】:

      【解决方案11】:

      这可能适用于短链接,文本视图中的多链接。它适用于 iOS 6、7、8。

      - (void)tappedTextView:(UITapGestureRecognizer *)tapGesture {
          if (tapGesture.state != UIGestureRecognizerStateEnded) {
              return;
          }
          UITextView *textView = (UITextView *)tapGesture.view;
          CGPoint tapLocation = [tapGesture locationInView:textView];
      
          NSDataDetector *detector = [NSDataDetector dataDetectorWithTypes:NSTextCheckingTypeLink|NSTextCheckingTypePhoneNumber
                                                                 error:nil];
          NSArray* resultString = [detector matchesInString:self.txtMessage.text options:NSMatchingReportProgress range:NSMakeRange(0, [self.txtMessage.text length])];
          BOOL isContainLink = resultString.count > 0;
      
          if (isContainLink) {
              for (NSTextCheckingResult* result in  resultString) {
                  CGRect linkPosition = [self frameOfTextRange:result.range inTextView:self.txtMessage];
      
                  if(CGRectContainsPoint(linkPosition, tapLocation) == 1){
                      if (result.resultType == NSTextCheckingTypePhoneNumber) {
                          NSString *phoneNumber = [@"telprompt://" stringByAppendingString:result.phoneNumber];
                          [[UIApplication sharedApplication] openURL:[NSURL URLWithString:phoneNumber]];
                      }
                      else if (result.resultType == NSTextCheckingTypeLink) {
                          [[UIApplication sharedApplication] openURL:result.URL];
                      }
                  }
              }
          }
      }
      
       - (CGRect)frameOfTextRange:(NSRange)range inTextView:(UITextView *)textView
      {
          UITextPosition *beginning = textView.beginningOfDocument;
          UITextPosition *start = [textView positionFromPosition:beginning offset:range.location];
          UITextPosition *end = [textView positionFromPosition:start offset:range.length];
          UITextRange *textRange = [textView textRangeFromPosition:start toPosition:end];
          CGRect firstRect = [textView firstRectForRange:textRange];
          CGRect newRect = [textView convertRect:firstRect fromView:textView.textInputView];
          return newRect;
      }
      

      【讨论】:

      • 在 iOS 9 中通过点击手势检测点击不适用于连续点击。
      【解决方案12】:

      这在 iOS 10 中发生了变化。在 iOS 10 中,您可以使用 .link 属性,一切正常。

      不需要自定义属性、点击手势识别器或任何东西。它就像一个普通的 URL。

      为此,不要将 url 添加到 NSMutableAttributedString,而是添加您想要调用 url 的内容(例如,'cats' 转到关于猫的维基百科页面),然后添加标准属性 NSAttributedString.Key .link(我在这里使用 Swift),其中 NSURL 包含目标 URL。

      参考:https://medium.com/real-solutions-artificial-intelligence/create-clickable-links-with-nsmutableattributedstring-12b6661a357d

      【讨论】:

        猜你喜欢
        • 2016-02-13
        • 1970-01-01
        • 2013-09-13
        • 2012-06-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多