Rileva i tocchi sul testo attribuito in un UITextView in iOS

Ho un UITextView che visualizza un NSAttributedString. Questa stringa contiene le parole che mi piacerebbe rendere toccabili, in modo tale che quando vengono toccate vengono richiamate in modo che io possa eseguire un’azione. Mi rendo conto che UITextView può rilevare i tocchi su un URL e richiamare il mio delegato, ma questi non sono URL.

Mi sembra che con iOS7 e la potenza di TextKit questo sia ora ansible, tuttavia non riesco a trovare alcun esempio e non sono sicuro da dove cominciare.

Capisco che è ora ansible creare attributi personalizzati nella stringa (anche se non l’ho ancora fatto), e forse questi saranno utili per scoprire se una delle parole magiche è stata sfruttata? In ogni caso, non so ancora come intercettare quel touch e rilevare su quale parola si è verificato il rubinetto.

Nota che la compatibilità con iOS 6 non è richiesta.

Volevo solo aiutare gli altri un po ‘di più. Seguendo la risposta di Shmidt è ansible fare esattamente come avevo chiesto nella mia domanda iniziale.

1) Creare una stringa attribuita con attributi personalizzati applicati alle parole cliccabili. per esempio.

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

2) Creare un UITextView per visualizzare quella stringa e aggiungervi un UITapGestureRecognizer. Quindi gestisci il rubinetto:

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

Così facile quando sai come!

Aggiornato per Swift 3

Rileva i tocchi sul testo attribuito con Swift

A volte per i principianti è un po ‘difficile sapere come fare a mettere le cose a posto (lo era comunque per me), quindi questo esempio è un po’ più pieno e usa Swift 3.

Aggiungi una UITextView al tuo progetto.

inserisci la descrizione dell'immagine qui

impostazioni

Utilizza le seguenti impostazioni nella finestra di ispezione Attributi:

inserisci la descrizione dell'immagine qui

inserisci la descrizione dell'immagine qui

Presa

Colbind UITextView al UITextView con una presa denominata textView .

Codice

Aggiungi il codice al tuo View Controller per rilevare il touch. Nota 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)") } } } } 

inserisci la descrizione dell'immagine qui

Ora se tocchi la "w" di "Swift", dovresti ottenere il seguente risultato:

inserisci la descrizione dell'immagine qui

Gli appunti

  • Qui ho usato un attributo personalizzato, ma avrebbe potuto essere altrettanto facilmente NSForegroundColorAttributeName (colore del testo) che ha un valore di UIColor.greenColor() .
  • Funziona solo se la visualizzazione del testo è impostata come non modificabile e non selezionabile, come descritto nella precedente sezione Impostazioni. Rendendolo modificabile e selezionabile è la ragione del problema discusso nei commenti qui sotto.

Ulteriore studio

Questa risposta era basata su diverse altre risposte a questa domanda. Oltre a questi, vedi anche

  • Layout ed effetti di testo avanzato con kit di testo (video WWDC 2013)
  • Guida alla programmazione delle stringhe attribuita
  • Come posso creare una stringa attribuita usando Swift?

Questa è una versione leggermente modificata, che si basa sulla risposta di @tarmes. Non ho potuto ottenere la variabile value per restituire nulla ma null senza il tweak qui sotto. Inoltre, avevo bisogno del dizionario degli attributi completo restituito per determinare l’azione risultante. Avrei messo questo nei commenti ma non sembra che il rappresentante lo faccia. Mi scuso in anticipo se ho violato il protocollo.

Il tweak specifico è usare textView.textStorage invece di textView.attributedText . Come programmatore iOS ancora in fase di apprendimento, non sono davvero sicuro del perché questo sia, ma forse qualcun altro può illuminarci.

Modifica specifica nel metodo di gestione del rubinetto:

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

Codice completo nel mio controller di visualizzazione

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

Creare collegamenti personalizzati e fare ciò che vuoi al touch è diventato molto più semplice con iOS 7. Esiste un ottimo esempio in Ray Wenderlich

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

Sono stato in grado di risolvere questo semplicemente con 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 } } 

È ansible farlo con characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints: Funzionerà in un modo diverso da quello che volevi: dovrai testare se un personaggio sfruttato appartiene a una parola magica . Ma non dovrebbe essere complicato.

Consiglio vivamente di guardare l’ introduzione del kit di testo del WWDC 2013.

Esempio completo per rilevare azioni sul testo attribuito con 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 } 

E poi puoi prendere l’azione con shouldInteractWith URL UITextViewDelegate metodo delegato. Quindi assicurati di aver impostato correttamente il delegato.

 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 } 

Come saggio, puoi eseguire qualsiasi azione in base alle tue esigenze.

Saluti!!

Questo potrebbe funzionare bene con lo short link, multilink in una textview. Funziona bene con 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; } 

Con Swift 4 e iOS 11, puoi creare una sottoclass di UITextView e sovrascrivere hitTest(_:with:) o point(inside:with:) con alcune implementazioni di TextKit per fare in modo che solo alcune NSAttributedStrings siano intercettabili.


Il codice seguente mostra come creare un UITextView che reagisce solo ai tocchi su NSAttributedStrings sottolineati in esso:

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