可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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!
回答2:
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.
Add a UITextView
to your project.
Outlet
Connect the UITextView
to the ViewController
with an outlet named textView
.
Custom attribute
We are going to make a custom attribute by making an 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.
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.
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
.
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)\")
}
}
}
}
Now if you tap on the \"w\" of \"Swift\", you should get the following result:
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.
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?
回答3:
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
}
}
回答4:
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
回答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:
}
回答6:
I was able to solve this pretty simply with 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
}
}
回答7:
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.
BTW I highly recommend watching Introducing Text Kit from WWDC 2013.
回答8:
Complete example for detect actions on attributed text with 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 URL
UITextViewDelegate delegate method.So make sure you have set the delegate properly.
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!!
回答9:
This one might work OK with short link, multilink in a textview. It work OK with 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;
}
回答10:
With Swift 4 and iOS 11, you can create a subclass of UITextView
and override hitTest(_:with:)
or point(inside:with:)
with some TextKit implementation in order to make only some NSAttributedStrings
in it tappable.
The following code shows how to create a UITextView
that only reacts to taps on underlined NSAttributedStrings
in it:
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 hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let characterIndex = layoutManager.characterIndex(for: point, in: textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
guard characterIndex < textStorage.length else { return nil }
let attributes = textStorage.attributes(at: characterIndex, effectiveRange: nil)
return attributes[NSAttributedStringKey.underlineStyle] != nil ? self : nil
}
/*
// Alternative using point(inside:with:)
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
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 attributes[NSAttributedStringKey.underlineStyle] != nil
}
*/
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let linkTextView = InteractiveUnderlinedTextView()
let mutableAttributedString = NSMutableAttributedString(string: \"Some text\\n\\n\\n\")
let attributes = [NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.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\")
}
}