Seleccionar una palabra en una UITextView

EDIT: Estoy pensando que debería usar una UILabel en lugar de una UITextView, ya que no quiero que el resaltado use el sistema ancho azul con popover 'copyr / seleccionar todo / definir'.

Estoy intentando crear una vista personalizada de text, y estoy buscando ayuda con el enfoque correcto. Soy un iOS n00b, así que busco principalmente ideas sobre cómo abordar mejor el problema.

Quiero crear una vista de text personalizada, con el siguiente comportamiento:

  1. La vista comienza en pequeño. Si recibe un toque, éste crece (animado) al tamaño de la vista principal como una window modal (image superior izquierda, luego image superior derecha)
  2. En este estado grande, si se toca una palabra individual, la palabra se destaca de alguna manera, y se llama a un método delegado pasando una cadena que contiene la que se tocó (image inferior izquierda)

Nueva vista de text http://telliott.net/static/NewTextView.png

Sospecho que la complejidad va a estar en la identificación de la palabra que se hizo clic, así que empecemos allí. Se me ocurren dos forms de probar esto:

  1. Subclass UILabel. Agregue un reconocimiento de gestos táctil. Cuando se identifica un toque, obtenga las coorderadas (xy) del punto táctil de alguna manera, mire la cadena que muestra la vista y descubra qué palabra debe haberse presionado en function de la position. Sin embargo, puedo ver que esto se vuelve muy complicado bastante rápido con el envoltorio de palabras. No estoy seguro de cómo get las coorderadas (x, y) del toque (aunque supongo que eso es bastante simple), o cómo get anchos de text dependientes del dispositivo para cada carácter, etc. Preferiría no seguir esta ruta a less que ¡Alguien puede convencerme de que no va a ser horrible!
  2. Subclass UIView, y falsifique la oración agregando un UILabel para cada palabra. Mida el ancho de cada UILabel y colóquelos yo mismo. Esto parece un enfoque más sensato, aunque me preocupa que exponer el text de esta manera también sea más difícil de lo que creo cuando empiece a intentarlo.

¿Hay alguna manera más sensata? ¿Puedes recuperar una palabra que fue tocada en un UILabel de alguna otra manera?

Si voy por la opción 2, creo que la animation de pequeños -> text grande puede ser complicada, ya que las palabras se envolverán de maneras interesantes. Así que estaba pensando en tener otra subvista, esta vez una UITextView, para mantener la oración en el pequeño estado. Animar esto a grande, luego ocultarlo, y al mismo time revelar mi UIView con one-view-per-word.

Cualquier pensamiento apreciado. Gracias por adelantado 🙂

Actualizar

Esto fue mucho más fácil para iOS 7, gracias a la incorporación de NSLayoutManager en CoreText. Si está tratando con un UITextView, puede acceder al administrador de layout como una propiedad de la vista. En mi caso, quería seguir con una UILabel, por lo que debe crear un administrador de layout con el mismo tamaño, es decir:

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:labelText]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; CGRect bounds = label.bounds; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:bounds.size]; [layoutManager addTextContainer:textContainer]; 

Ahora solo necesita encontrar el índice del personaje que hizo clic, ¡que es simple!

 NSUInteger characterIndex = [layoutManager characterIndexForPoint:location inTextContainer:textContainer fractionOfDistanceBetweenInsertionPoints:NULL]; 

Lo que hace que sea trivial encontrar la palabra en sí misma:

 if (characterIndex < textStorage.length) { [labelText.string enumerateSubstringsInRange:NSMakeRange(0, textStorage.length) options:NSStringEnumerationByWords usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { if (NSLocationInRange(characterIndex, enclosingRange)) { // Do your thing with the word, at range 'enclosingRange' *stop = YES; } }]; } 

Respuesta original, que funciona para iOS <7

Gracias a @JP Hribovsek por algunos consejos para que esto funcione, logré resolver esto lo suficientemente bien para mis propósitos. Se siente un poco hacky, y probablemente no funcione demasiado bien para grandes cuerpos de text, pero para párrafos a la vez (que es lo que necesito) está bien.

Creé una subclass UILabel simple que me permite establecer el valor de inserción:

 #import "WWLabel.h" #define WWLabelDefaultInset 5 @implementation WWLabel @synthesize topInset, leftInset, bottomInset, rightInset; - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { self.topInset = WWLabelDefaultInset; self.bottomInset = WWLabelDefaultInset; self.rightInset = WWLabelDefaultInset; self.leftInset = WWLabelDefaultInset; } return self; } - (void)drawTextInRect:(CGRect)rect { UIEdgeInsets insets = {self.topInset, self.leftInset, self.bottomInset, self.rightInset}; return [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)]; } 

Luego, creé una subclass UIView que contenía mi label personalizada y, con un toque, construí el tamaño del text para cada palabra en la label, hasta que el tamaño superó el tamaño de la location del toque; esta es la palabra que se tocó. No es prefecto, pero funciona bastante bien por ahora.

Luego utilicé un simple NSAttributedString para resaltar el text:

 #import "WWPhoneticTextView.h" #import "WWLabel.h" #define WWPhoneticTextViewInset 5 #define WWPhoneticTextViewDefaultColor [UIColor blackColor] #define WWPhoneticTextViewHighlightColor [UIColor yellowColor] #define UILabelMagicTopMargin 5 #define UILabelMagicLeftMargin -5 @implementation WWPhoneticTextView { WWLabel *label; NSMutableAttributedString *labelText; NSRange tappedRange; } // ... skipped init methods, very simple, just call through to configureView - (void)configureView { if(!label) { tappedRange.location = NSNotFound; tappedRange.length = 0; label = [[WWLabel alloc] initWithFrame:[self bounds]]; [label setLineBreakMode:NSLineBreakByWordWrapping]; [label setNumberOfLines:0]; [label setBackgroundColor:[UIColor clearColor]]; [label setTopInset:WWPhoneticTextViewInset]; [label setLeftInset:WWPhoneticTextViewInset]; [label setBottomInset:WWPhoneticTextViewInset]; [label setRightInset:WWPhoneticTextViewInset]; [self addSubview:label]; } // Setup tap handling UITapGestureRecognizer *singleFingerTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)]; singleFingerTap.numberOfTapsRequinetworking = 1; [self addGestureRecognizer:singleFingerTap]; } - (void)setText:(NSString *)text { labelText = [[NSMutableAttributedString alloc] initWithString:text]; [label setAttributedText:labelText]; } - (void)handleSingleTap:(UITapGestureRecognizer *)sender { if (sender.state == UIGestureRecognizerStateEnded) { // Get the location of the tap, and normalise for the text view (no margins) CGPoint tapPoint = [sender locationInView:sender.view]; tapPoint.x = tapPoint.x - WWPhoneticTextViewInset - UILabelMagicLeftMargin; tapPoint.y = tapPoint.y - WWPhoneticTextViewInset - UILabelMagicTopMargin; // Iterate over each word, and check if the word contains the tap point in the correct line __block NSString *partialString = @""; __block NSString *lineString = @""; __block int currentLineHeight = label.font.pointSize; [label.text enumerateSubstringsInRange:NSMakeRange(0, [label.text length]) options:NSStringEnumerationByWords usingBlock:^(NSString* word, NSRange wordRange, NSRange enclosingRange, BOOL* stop){ CGSize sizeForText = CGSizeMake(label.frame.size.width-2*WWPhoneticTextViewInset, label.frame.size.height-2*WWPhoneticTextViewInset); partialString = [NSString stringWithFormat:@"%@ %@", partialString, word]; // Find the size of the partial string, and stop if we've hit the word CGSize partialStringSize = [partialString sizeWithFont:label.font constrainedToSize:sizeForText lineBreakMode:label.lineBreakMode]; if (partialStringSize.height > currentLineHeight) { // Text wrapped to new line currentLineHeight = partialStringSize.height; lineString = @""; } lineString = [NSString stringWithFormat:@"%@ %@", lineString, word]; CGSize lineStringSize = [lineString sizeWithFont:label.font constrainedToSize:label.frame.size lineBreakMode:label.lineBreakMode]; lineStringSize.width = lineStringSize.width + WWPhoneticTextViewInset; if (tapPoint.x < lineStringSize.width && tapPoint.y > (partialStringSize.height-label.font.pointSize) && tapPoint.y < partialStringSize.height) { NSLog(@"Tapped word %@", word); if (tappedRange.location != NSNotFound) { [labelText addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:tappedRange]; } tappedRange = wordRange; [labelText addAttribute:NSForegroundColorAttributeName value:[UIColor networkingColor] range:tappedRange]; [label setAttributedText:labelText]; *stop = YES; } }]; } } 

UITextView ya tiene un método de delegado que se activa cuando cambia la selección (tenga en count que mover el cursor dentro de la vista de text es equivalente a cambiar la selección, el usuario realmente no necesita 'seleccionar' ningún text para que se llame):

 - (void)textViewDidChangeSelection:(UITextView *)textView 

Cada vez que esto se dispara, obtén el range seleccionado así:

 NSRange range=textView.selectedRange; 

Si el usuario debe poder mover el cursor manualmente o seleccionar una palabra completa, entonces está prácticamente terminado; de lo contrario, simplemente agregue algo de procesamiento alnetworkingedor de la cadena en selectedRange para averiguar cuál es la palabra alnetworkingedor del cursor y resáltelo con tu método de elección.
Por ejemplo, puede enumerar todas las palabras en la vista de text y descubrir cuál contiene la selección actual (o cursor) y seleccionar la palabra completa (que es una forma de resaltar el iOS 6 anterior)

 - (void)textViewDidChangeSelection:(UITextView *)textView{ NSRange range=textView.selectedRange; [textView.text enumerateSubstringsInRange:NSMakeRange(0, [textView.text length]) options:NSStringEnumerationByWords usingBlock:^(NSString* word, NSRange wordRange, NSRange enclosingRange, BOOL* stop){ NSRange intersectionRange=NSIntersectionRange(range,wordRange); if(interesectionRange.length>0){ [textView setSelectedRange:wordRange]; } }]; }