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

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

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

我知道现在可以在字符串中创建自定义属性(尽管我还没有这样做) ,也许这些对于检测是否有一个神奇的单词被点击有用?无论如何,我还是不知道如何拦截那个窃听,并检测出哪个单词发生了窃听。

请注意,iOS6兼容性是 没有所必需的。

75508 次浏览

characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints:可以做到这一点。它的工作方式与您想要的有所不同——您将不得不测试一个被点击的字符是否属于 魔法咒语。但不应该这么复杂。

顺便说一句,我强烈推荐观看2013年 WWDC 的 文字资料套介绍

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

我只是想多帮助别人一点。根据施密特的回答,完全按照我在最初的问题中提出的要求去做是可能的。

1)创建一个属性化字符串,自定义属性应用于可点击的 words。

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

2)创建一个 UITextView 来显示这个字符串,并在其中添加一个 UITapGestureIdentiizer,然后点击:

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


}
}

当你知道怎么做的时候就容易多了!

这是一个稍微修改过的版本,基于@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


}
}

在 iOS7中创建自定义链接并随心所欲地做你想做的事情变得更加容易。 这里有一个非常好的例子,就是 Ray Wenderlich

这个可以在文本视图中使用短链接和多链接,也可以在 iOS6,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;
}

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

有时候对于初学者来说,要知道如何设置一些东西是有点困难的(至少对我来说是这样) ,所以这个例子比较全面。

向项目中添加 UITextView

出口

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

自定义属性

我们将通过创建一个 分机来创建一个自定义属性。

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

添加一个新的快捷文件与 文件 > New > File... > iOS > Source > Swift File。你可以叫它你想要的。我叫我的 NSAttributedStringKey + CustomAttribute.ift

粘贴以下代码:

import Foundation


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

密码

用以下代码替换 ViewController.swift 中的代码。

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

现在,如果你点击“ 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的答案中,不管这些是否被选择,它似乎都工作得很好。

进一步研究

这个答案是基于对这个问题的其他几个答案

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

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


}

使用 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 UITextViewgenerate 委托方法捕获该操作。

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
}

同样明智的是,你可以根据自己的需求采取任何行动。

干杯!

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


下面的代码显示了如何创建一个仅对带下划线的 NSAttributedString上的点击作出反应的 UITextView:

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


}

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

从 iOS10开始,这种情况发生了改变。在 iOS10中,你可以使用。链接属性,它只是工作。

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

要做到这一点,不要将 url 添加到 NSMutableAttributedString,而是添加您想要调用的 url (例如,“ cat”转到关于 cat 的维基百科页面) ,然后添加标准属性 NSAttributedString。Link (我在这里使用的是 Swift) ,使用包含目标 URL 的 NSURL。

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