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


122

我有一个UITextView显示的NSAttributedString。该字符串包含我想使其可轻敲的单词,以便在点击它们时会回叫我以便执行操作。我意识到UITextView可以检测到URL上的轻击并回叫我的代表,但这不是URL。

在我看来,借助iOS 7和TextKit的强大功能,现在应该可以实现这一点,但是我找不到任何示例,而且我不确定从哪里开始。

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

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

Answers:


118

我只是想帮助别人多一点。根据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/…–
Steaphann

实际上characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints在iOS 6上可用,所以我认为它应该可以工作。让我们知道!参见该项目的示例:github.com/laevandus/NSTextFieldHyperlinks/blob/master/…– tarmes 2013
16:37

文档说它仅在IOS 7或更高版本中可用:)
Steaphann

1
是的对不起 我对Mac OS感到困惑!这仅是iOS7。
tarmes

当您无法选择UITextView时,它似乎不起作用
Paul Brewczynski 2014年

64

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

有时候,对于初学者而言,很难知道如何进行设置(无论如何对我来说),因此该示例更加完整。

将一个添加UITextView到您的项目。

出口

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

自定义属性

我们将通过创建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

笔记

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

进一步研究

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


使用myTextView.textStorage代替 myTextView.attributedText.string
fatihyildizhan

在iOS 9中通过轻击手势检测轻击不适用于连续轻击。有任何更新吗?
Dheeraj Jami

1
@WaqasMahmood,我为此问题提出一个新问题。您可以将其加注星标,稍后再查看是否有任何答案。如果还有其他相关细节,请随时编辑该问题或添加评论。
Suragch 2015年

1
@dejix我通过每次在TextView的末尾添加另一个空字符串来解决此问题。这样,在您说完最后一个字之后,检测就会停止。希望对您
有所

1
多次点击即可完美运行,我只是在一个简短的例程中证明了这一点:if characterIndex <12 {textView.textColor = UIColor.magenta} else {textView.textColor = UIColor.blue}真正清晰简单的代码
Jeremy Andrews

32

这是一个略作修改的版本,以@tarmes答案为基础。我无法获得value变量以返回任何内容,但是null没有下面的调整。另外,我需要返回完整的属性字典才能确定所产生的操作。我本可以在评论中输入此内容,但似乎没有代表这样做。如果我违反协议,请提前致歉。

具体的调整方法是使用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提示!
Kai Burghardt 2014年

在iOS 9中通过轻击手势检测轻击不适用于连续轻击。
Dheeraj Jami

25

使用iOS 7,进行自定义链接和在水龙头上做您想做的事情变得更加容易。RayWenderlich有一个很好的例子


与尝试计算相对于容器视图的字符串位置相比,这是一种更清洁的解决方案。
克里斯C

2
问题是textView需要是可选的,我不希望这种行为。
托马斯·卡尔蒙

@ThomásC。+1指示UITextView即使我已将其设置为通过IB来检测链接,但为什么我仍未检测到链接。(我也使其无法选择)
Kedar Paranjape

13

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

谢谢!我也会看WWDC视频。
Tarmes

@Suragch“带有文本工具包的高级文本布局和效果”。
Shmidt

10

我可以用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 true之外的其他URL,if以便UITextView可以处理https://被窃听的常规链接
Daniel Storm

我做到了,它在iPhone 6和6+上都可以正常工作,但在iPhone 5上却根本不工作。从来没有发现为什么iPhone 5会有这个问题,这毫无道理。
n13

9

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

同样,您可以根据需要执行任何操作。

干杯!!


谢谢!你救了我的一天!
Dmih


4

使用Swift 5和iOS 12,您可以创建TextKit实现的子类UITextViewpoint(inside:with:)用某些TextKit实现覆盖,以便仅使其中的一些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")
    }

}

嗨,有什么方法可以使它符合多个属性,而不仅仅是一个?
David

1

短链接,文本视图中的多链接可能可以正常工作。在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中通过轻击手势检测轻击不适用于连续轻击。
Dheeraj Jami

1

将此扩展用于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: tapableText),
      tapGesture.didTapAttributedTextInTextView(textView: textTextView, inRange: NSRange(range, in: text)) {
                // Tap recognized
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.