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.
Es posible hacer eso con characterIndexForPoint:inTextContainer:fractionOfDistanceBetweenInsertionPoints:
Funcionará de forma algo diferente de lo que usted quería: tendrá que probar si un personaje intervenido pertenece a una palabra mágica . Pero no debería ser complicado.
Por cierto, recomiendo ver la introducción del kit de texto de la WWDC 2013.
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!
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 deUIColor.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
- Diseños de texto avanzados y efectos con el kit de texto (video WWDC 2013)
- Guía de programación de cadenas atribuidas
- ¿Cómo hago una cadena atribuida usando Swift?
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!!