Wykrywanie dotknięć przypisanego tekstu w widoku UITextView w systemie iOS
Mam UITextView, który wyświetla NSAttributedString. Ten ciąg zawiera słowa, które chciałbym zrobić tappable, takie, że gdy są one tapped i dostać wywołane z powrotem, tak, że mogę wykonać akcję. Zdaję sobie sprawę, że UITextView może wykryć krany na URL i oddzwonić do mojego delegata, ale to nie są adresy URL.
Wydaje mi się, że z iOS7 i mocą TextKit powinno to być teraz możliwe, jednak nie mogę znaleźć żadnych przykładów i nie jestem pewien, od czego zacząć.
Rozumiem że teraz możliwe jest tworzenie własnych atrybutów w łańcuchu (chociaż jeszcze tego nie robiłem), a może będą one przydatne do wykrycia, czy któryś z magicznych słów został stuknięty? W każdym razie nadal Nie wiem, jak przechwycić to dotknięcie i wykryć, w którym słowie nastąpiło dotknięcie.
Należy pamiętać, że zgodność z iOS 6 nie jest wymagana.
10 answers
Chciałam tylko pomóc innym trochę bardziej. Po odpowiedzi Shmidta można zrobić dokładnie to, o co prosiłem w swoim pierwotnym pytaniu.
1) Utwórz przypisany ciąg znaków z niestandardowymi atrybutami zastosowanymi do klikalnych słów. np.
NSAttributedString* attributedString = [[NSAttributedString alloc] initWithString:@"a clickable word" attributes:@{ @"myCustomTag" : @(YES) }];
[paragraph appendAttributedString:attributedString];
2) Utwórz UITextView, aby wyświetlić ten ciąg znaków i dodaj do niego UITapGestureRecognizer. Następnie uchwyć Kran:
- (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);
}
}
Tak łatwo, gdy wiesz jak!Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-22 07:50:36
Aktualizacja dla Swift 3
Wykrywanie kranu na przypisanym tekście za pomocą Swift
Czasami dla początkujących trochę trudno jest wiedzieć ,jak skonfigurować rzeczy (dla mnie i tak było), więc ten przykład jest trochę pełniejszy i używa Swift 3.
Dodaj UITextView
do swojego projektu.
Ustawienia
Użyj następujących ustawień w atrybutach Inspektor:
Outlet
Podłącz {[1] } do ViewController
za pomocą gniazda o nazwie textView
.
Kod
Dodaj kod do kontrolera widoku, aby wykryć stuknięcie. Zwróć uwagę na 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 = [ "MyCustomAttributeName": "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)
}
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 = "MyCustomAttributeName"
let attributeValue = myTextView.attributedText.attribute(attributeName, at: characterIndex, effectiveRange: nil) as? String
if let value = attributeValue {
print("You tapped on \(attributeName) and the value is: \(value)")
}
}
}
}
Teraz, jeśli dotkniesz "W" Z "Swift", powinieneś uzyskać następujący wynik:
Uwagi
- tutaj użyłem niestandardowego atrybutu, ale to równie łatwo mogło być
NSForegroundColorAttributeName
(kolor tekstu) o wartościUIColor.greenColor()
. - to działa tylko wtedy, gdy Widok tekstowy jest ustawiony jako nie edytowalny inie wybierany, jak opisano w sekcji Ustawienia powyżej. Uczynienie go edytowalnym i wybieralnym jest powodem problemu omówionego w komentarzach poniżej.
Dalsze badania
Odpowiedź ta została oparta na kilku innych odpowiedziach na to pytanie. Oprócz nich, Zobacz także
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:13
Jest to nieco zmodyfikowana wersja, bazująca na @tarmes answer. Nie mogłem uzyskać zmiennej value
, która zwróci cokolwiek poza null
bez zmiany poniżej. Potrzebowałem również zwrotu pełnego słownika atrybutów, aby określić wynikającą akcję. Ja bym to zamieścił w komentarzach, ale chyba nie mam do tego rep. Z góry przepraszam, jeśli złamałem protokół.
Specyficzną modyfikacją jest użycie textView.textStorage
zamiast textView.attributedText
. Jako wciąż uczący się programista iOS nie jestem naprawdę wiem, dlaczego tak jest, ale może ktoś inny nas oświeci.
Specyficzna modyfikacja w metodzie obsługi tap:
NSDictionary *attributesOfTappedText = [textView.textStorage attributesAtIndex:characterIndex effectiveRange:&range];
Pełny kod w moim kontrolerze widoku
- (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
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-01 18:59:07
Tworzenie niestandardowego łącza i robienie tego, co chcesz na kranu, stało się znacznie łatwiejsze dzięki iOS 7. Jest bardzo dobry przykład w Ray Wenderlich
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-22 07:40:30
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:
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-27 00:56:28
Udało mi się rozwiązać to całkiem po prostu za pomocą 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
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-01-07 01:45:40
Można to zrobić z characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints:
. Będzie działać nieco inaczej niż chciałeś - będziesz musiał sprawdzić, czy stuknięta postać należy do magicznego słowa . Ale to nie powinno być skomplikowane.
BTW Gorąco polecam obejrzeć Wprowadzenie Text Kit z WWDC 2013.
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-10-12 09:22:24
Kompletny przykład wykrywania akcji na przypisanym tekście za pomocą 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
}
A następnie możesz złapać akcję za pomocą shouldInteractWith URL
uitextviewdelegate delegate method.So upewnij się, że poprawnie Ustawiłeś delegata.
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
}
Podobnie jak wise możesz wykonać dowolną czynność zgodnie ze swoimi wymaganiami.
Zdrówko!!Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-28 10:57:20
Ten może działać OK z krótkim linkiem, multilink w widoku tekstowym. Działa OK z 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;
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-11-26 01:20:48
Dzięki Swift 4 i iOS 11 możesz utworzyć podklasę UITextView
i nadpisać hitTest(_:with:)
lub point(inside:with:)
z pewną implementacją TextKit, aby tylko niektóre NSAttributedStrings
mogły być w niej użyte.
Poniższy kod pokazuje, jak utworzyć UITextView
, który reaguje tylko na naciśnięcia na podkreślone NSAttributedStrings
in it:
interactive Underlinedtextview.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")
}
}
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-02 08:43:20