ios objective-c uitextview textkit

Detección de toques en texto atribuido en una UITextView en iOS



objective-c textkit (9)

Éste podría funcionar bien con enlace corto, multienlace en una vista de texto. Funciona bien 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; }

Tengo un UITextView que muestra un NSAttributedString. Esta cadena contiene palabras que me gustaría hacer tappables, de modo que cuando se toquen me devuelvan la llamada para que pueda realizar una acción. Me doy cuenta de que UITextView puede detectar toques en una URL y devolver la llamada a mi delegado, pero estos no son URL.

Me parece que con iOS7 y el poder de TextKit ahora debería ser posible, pero no puedo encontrar ningún ejemplo y no estoy seguro de por dónde empezar.

Entiendo que ahora es posible crear atributos personalizados en la cadena (aunque todavía no lo he hecho), y quizás estos sean útiles para detectar si se ha tocado una de las palabras mágicas. En cualquier caso, todavía no sé cómo interceptar ese toque y detectar en qué palabra ocurrió el toque.

Tenga en cuenta que la compatibilidad con iOS 6 no es necesaria.



Esta es una versión ligeramente modificada, basada en la respuesta de @tarmes. No pude obtener la variable de value para devolver nada más que null sin el ajuste a continuación. Además, necesitaba el diccionario de atributos completo devuelto para determinar la acción resultante. Hubiera puesto esto en los comentarios, pero no parece tener el representante para hacerlo. Disculpas de antemano si he violado el protocolo.

El ajuste específico es usar textView.textStorage lugar de textView.attributedText . Como un programador de iOS que todavía está aprendiendo, no estoy muy seguro de por qué es así, pero tal vez alguien más nos puede iluminar.

Modificación específica en el método de manejo del grifo:

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

Código completo en mi controlador de vista

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


Hacer un enlace personalizado y hacer lo que quieras en el grifo se ha vuelto mucho más fácil con iOS 7. Hay un muy buen ejemplo en Ray Wenderlich


Pude resolver esto simplemente 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 } }


Solo quería ayudar a otros un poco más. Después de la respuesta de Shmidt, es posible hacer exactamente lo que había pedido en mi pregunta original.

1) Cree una cadena atribuida con atributos personalizados aplicados a las palabras clicables. p.ej.

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

2) Cree un UITextView para mostrar esa cadena y añádale un UITAPGestureRecognizer. Luego maneja el 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); } }

¡Tan fácil cuando sabes cómo!


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


Actualizado para Swift 3

Detección de toques en texto atribuido con Swift

A veces, para los principiantes, es un poco difícil saber cómo hacer que las cosas se configuren (era para mí de todos modos), así que este ejemplo es un poco más completo y usa Swift 3.

Agregue un UITextView a su proyecto.

Configuraciones

Use la siguiente configuración en el inspector de atributos:

Salida

Conecte el UITextView al ViewController con un outlet llamado textView .

Código

Agregue código a su Controlador de Vista para detectar el toque. Tenga en cuenta el 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)") } } } }

Ahora, si tocas la "w" de "Swift", deberías obtener el siguiente resultado:

Notas

  • Aquí utilicé un atributo personalizado, pero podría haber sido fácilmente NSForegroundColorAttributeName (color de texto) que tiene un valor de UIColor.greenColor() .
  • Esto solo funciona si la vista de texto está configurada como no editable y no seleccionable, tal como se describe en la sección Configuración anterior. Hacerlo editable y seleccionable es la razón del problema discutido en los comentarios a continuación.

Estudio adicional

Esta respuesta se basó en varias otras respuestas a esta pregunta. Además de estos, vea también


Ejemplo completo para detectar acciones en texto atribuido 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 }

Y luego puede ver la acción con shouldInteractWith URL UITextViewDelegate método de delegado. Así que asegúrese de haber configurado correctamente el delegado.

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 }

De la misma manera, puedes realizar cualquier acción de acuerdo a tus requerimientos.

¡¡Aclamaciones!!