ios - textfielddidchange
UITextView cursor debajo del cuadro al cambiar el cuadro (9)
Tengo un UIViewCOntroller
que contiene un UITextView
. Cuando aparece el teclado lo redimensiono así:
#pragma mark - Responding to keyboard events
- (void)keyboardDidShow:(NSNotification *)notification
{
NSDictionary* info = [notification userInfo];
CGRect keyboardSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect newTextViewFrame = self.textView.frame;
newTextViewFrame.size.height -= keyboardSize.size.height + 70;
self.textView.frame = newTextViewFrame;
self.textView.backgroundColor = [UIColor yellowColor];
}
- (void)keyboardWillHide:(NSNotification *)notification
{
NSDictionary* info = [notification userInfo];
CGRect keyboardSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect newTextViewFrame = self.textView.frame;
newTextViewFrame.size.height += keyboardSize.size.height - 70;
self.textView.frame = newTextViewFrame;
}
El textView parece rezise al tamaño correcto, pero cuando el usuario escribe, el cursor termina "fuera" del marco textView. Vea la imagen a continuación:
El área amarilla es el marco de UITextView
(no sé cuál es la línea azul al lado de la tecla R). Encuentro esto bastante cableado. Estoy usando iOS7 si eso hace alguna diferencia.
¿Alguna idea o consejo?
Actualizar
Tengo una subclase UITextView que dibuja líneas horizontales con el siguiente método (si eso hace alguna diferencia):
- (void)drawRect:(CGRect)rect {
//Get the current drawing context
CGContextRef context = UIGraphicsGetCurrentContext();
//Set the line color and width
CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:229.0/255.0 green:244.0/255.0 blue:255.0/255.0 alpha:1].CGColor);
CGContextSetLineWidth(context, 1.0f);
//Start a new Path
CGContextBeginPath(context);
//Find the number of lines in our textView + add a bit more height to draw lines in the empty part of the view
NSUInteger numberOfLines = (self.contentSize.height + rect.size.height) / self.font.lineHeight;
CGFloat baselineOffset = 6.0f;
//iterate over numberOfLines and draw each line
for (int x = 0; x < numberOfLines; x++) {
//0.5f offset lines up line with pixel boundary
CGContextMoveToPoint(context, rect.origin.x, self.font.lineHeight*x + 0.5f + baselineOffset);
CGContextAddLineToPoint(context, rect.size.width, self.font.lineHeight*x + 0.5f + baselineOffset);
}
// Close our Path and Stroke (draw) it
CGContextClosePath(context);
CGContextStrokePath(context);
}
Anders y Leo Natan tienen grandes soluciones. Sin embargo, tuve que modificar un poco sus respuestas para que el desplazamiento funcione correctamente con contentInset. El problema al que me enfrenté fue que textViewDidBeginEditing:
se llama antes de keyboardWasShown:
para que el cambio de contentInset no se refleje la primera vez. Aquí esta lo que hice:
En .h
@interface NoteDayViewController : UIViewController <UITextViewDelegate>
{
UIEdgeInsets noteTextViewInsets;
UIEdgeInsets noteTextViewScrollIndicatorInsets;
CGRect oldRect;
NSTimer *caretVisibilityTimer;
float noteViewBottomInset;
}
@property (weak, nonatomic) IBOutlet UITextView *noteTextView;
En m
- (void)registerForKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification object:nil];
}
- (void)keyboardWasShown:(NSNotification*)aNotification
{
CGFloat kbHeight = // get the keyboard height following your usual method
UIEdgeInsets contentInsets = noteTextViewInsets;
contentInsets.bottom = kbHeight;
noteTextView.contentInset = contentInsets;
UIEdgeInsets scrollInsets = noteTextViewScrollIndicatorInsets;
scrollInsets.bottom = kbHeight;
noteTextView.scrollIndicatorInsets = scrollInsets;
[noteTextView setNeedsDisplay];
}
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
noteTextView.contentInset = noteTextViewInsets;
noteTextView.scrollIndicatorInsets = noteTextViewScrollIndicatorInsets;
[noteTextView setNeedsDisplay];
}
- (void)textViewDidBeginEditing:(UITextView *)textView
{
oldRect = [noteTextView caretRectForPosition:noteTextView.selectedTextRange.end];
noteViewBottomInset = noteTextView.contentInset.bottom;
caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(scrollCaretToVisible) userInfo:nil repeats:YES];
}
- (void)textViewDidEndEditing:(UITextView *)textView
{
[caretVisibilityTimer invalidate];
caretVisibilityTimer = nil;
}
- (void)scrollCaretToVisible
{
// This is where the cursor is at.
CGRect caretRect = [noteTextView caretRectForPosition:noteTextView.selectedTextRange.end];
// test if the caret has moved OR the bottom inset has changed
if(CGRectEqualToRect(caretRect, oldRect) && noteViewBottomInset == noteTextView.contentInset.bottom)
return;
// reset these for next time this method is called
oldRect = caretRect;
noteViewBottomInset = noteTextView.contentInset.bottom;
// this is the visible rect of the textview.
CGRect visibleRect = noteTextView.bounds;
visibleRect.size.height -= (noteTextView.contentInset.top + noteTextView.contentInset.bottom);
visibleRect.origin.y = noteTextView.contentOffset.y;
// We will scroll only if the caret falls outside of the visible rect.
if (!CGRectContainsRect(visibleRect, caretRect))
{
CGPoint newOffset = noteTextView.contentOffset;
newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height, 0);
[noteTextView setContentOffset:newOffset animated:NO]; // must be non-animated to work, not sure why
}
}
El comentario anterior de Angel Naydenov es correcto, especialmente en casos como el cambio del teclado inglés al japonés que se muestra sugiere.
Al cambiar de teclado, se llama a UIKeyboardWillHideNotification
pero no se llama a UIKeyboardWillHideNotification
.
Por lo tanto, debe ajustar la inserción para utilizar el valor absoluto y no utilizar +=
.
Sin relación, [self.textView setContentOffset:newOffset animated:YES];
no cambiará realmente los gráficos en iOS 7.1 después de que se muestre el teclado por segunda vez, lo que probablemente sea un error. Una solución que utilicé es reemplazar
[self.textView setContentOffset:newOffset animated:YES];
con
[UIView animateWithDuration:.25 animations:^{
self.textView.contentOffset = newOffset;
}];
En lugar de cambiar el tamaño del marco, ¿por qué no le da a su vista de texto un contentInset
(y un scrollIndicatorInsets
coincidente)? Recuerda que las vistas de texto son en realidad vistas de desplazamiento. Esta es la forma correcta de manejar la interferencia del teclado (u otra).
Para obtener más información sobre contentInset
, vea this pregunta.
Esto parece no ser suficiente. Aún use inserciones, ya que esto es más correcto (especialmente en iOS7, donde el teclado es transparente), pero también necesitará un manejo adicional para el caret:
- (void)viewDidLoad
{
[super viewDidLoad];
[self.textView setDelegate:self];
self.textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
}
- (void)_keyboardWillShowNotification:(NSNotification*)notification
{
UIEdgeInsets insets = self.textView.contentInset;
insets.bottom += [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
self.textView.contentInset = insets;
insets = self.textView.scrollIndicatorInsets;
insets.bottom += [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
self.textView.scrollIndicatorInsets = insets;
}
- (void)_keyboardWillHideNotification:(NSNotification*)notification
{
UIEdgeInsets insets = self.textView.contentInset;
insets.bottom -= [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
self.textView.contentInset = insets;
insets = self.textView.scrollIndicatorInsets;
insets.bottom -= [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
self.textView.scrollIndicatorInsets = insets;
}
- (void)textViewDidBeginEditing:(UITextView *)textView
{
_oldRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];
_caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(_scrollCaretToVisible) userInfo:nil repeats:YES];
}
- (void)textViewDidEndEditing:(UITextView *)textView
{
[_caretVisibilityTimer invalidate];
_caretVisibilityTimer = nil;
}
- (void)_scrollCaretToVisible
{
//This is where the cursor is at.
CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];
if(CGRectEqualToRect(caretRect, _oldRect))
return;
_oldRect = caretRect;
//This is the visible rect of the textview.
CGRect visibleRect = self.textView.bounds;
visibleRect.size.height -= (self.textView.contentInset.top + self.textView.contentInset.bottom);
visibleRect.origin.y = self.textView.contentOffset.y;
//We will scroll only if the caret falls outside of the visible rect.
if(!CGRectContainsRect(visibleRect, caretRect))
{
CGPoint newOffset = self.textView.contentOffset;
newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height + 5, 0);
[self.textView setContentOffset:newOffset animated:YES];
}
}
-(void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Mucho trabajo, Apple debería proporcionar una mejor manera de manejar el caret, pero esto funciona.
Esto es lo que terminé haciendo, y algo que parece funcionar:
- (void)textViewKeyboardWillShow:(NSNotification *)notification
{
NSDictionary* info = [notification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
// self.textViewBottomSpace.constant = NSLayoutConstraint in IB (bottom position)
self.textViewBottomSpace.constant = kbSize.height + 70;
[self.textView setNeedsDisplay];
}
- (void)textViewKeyboardWillHide:(NSNotification *)notification
{
self.textViewBottomSpace.constant = 0;
[self.textView setNeedsDisplay];
}
- (void)scrollCaretToVisible
{
//This is where the cursor is at.
CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];
if(CGRectEqualToRect(caretRect, _oldRect))
return;
_oldRect = caretRect;
//This is the visible rect of the textview.
CGRect visibleRect = self.textView.bounds;
visibleRect.size.height -= (self.textView.contentInset.top + self.textView.contentInset.bottom);
visibleRect.origin.y = self.textView.contentOffset.y;
//We will scroll only if the caret falls outside of the visible rect.
if(!CGRectContainsRect(visibleRect, caretRect)) {
CGPoint newOffset = self.textView.contentOffset;
newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height + 10, 0);
[self.textView setContentOffset:newOffset animated:YES];
}
}
- (void)textViewDidEndEditing:(UITextView *)textView
{
[_caretVisibilityTimer invalidate];
_caretVisibilityTimer = nil;
}
- (void)textViewDidBeginEditing:(UITextView *)textView
{
self.oldRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];
self.caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(scrollCaretToVisible) userInfo:nil repeats:YES];
}
Leo Natan, comenzaste bien pero tu ejecución fue relativamente ineficiente. Aquí hay una mejor manera de hacerlo con menos código:
// Add Keyboard Notification Listeners in ViewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
// And Add The Following Methods
- (void)_keyboardWillShowNotification:(NSNotification*)notification
{
CGRect textViewFrame = self.textView.frame;
textViewFrame.size.height -= ([notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height + 4.0);
self.textView.frame = textViewFrame;
}
- (void)_keyboardWillHideNotification:(NSNotification*)notification
{
CGRect textViewFrame = self.textView.frame;
textViewFrame.size.height += ([notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height + 4.0);
self.textView.frame = textViewFrame;
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
NSRange typingRange = NSMakeRange(textView.text.length - 1, 1);
[textView scrollRangeToVisible:typingRange];
return YES;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Muchas respuestas ya, encontré que en mi caso es en realidad mucho más simple. En el teclado , contentInset
que ajuste el contenido de la vista de contentInset
y mantengo el cuadro a pantalla completa. Y mientras scrollRangeToVisible:
no funciona para mí como para muchos otros, los métodos de vista de desplazamiento (de los cuales hereda UITextView) funcionan bien. Esto funciona para mí:
- (void)textViewDidChange:(UITextView *)textView
{
CGRect caret = [_textView caretRectForPosition:_textView.selectedTextRange.end];
[_textView scrollRectToVisible:caret animated:YES];
}
Para aquellos que tienen un UITextView
dentro de un UIScrollView
donde iOS <7 se encargó de desplazar el cursor a la vista: así es como funciona con iOS 7 (y también 5 y 6).
// This is the scroll view reference
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
// Track the current UITextView
@property (weak, nonatomic) UITextView *activeField;
- (void)textViewDidBeginEditing:(UITextView *)textView
{
self.activeField = textView;
}
- (void)textViewdDidEndEditing:(UITextView *)textView
{
self.activeField = nil;
}
// Setup the keyboard observers that take care of the insets & initial scrolling
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification object:nil];
- (void)keyboardWasShown:(NSNotification*)aNotification
{
// Set the insets above the keyboard
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
UIEdgeInsets insets = self.vForm.contentInset;
insets.bottom += kbSize.height;
self.vForm.contentInset = insets;
insets = self.vForm.scrollIndicatorInsets;
insets.bottom += kbSize.height;
self.vForm.scrollIndicatorInsets = insets;
// Scroll the active text field into view
CGRect aRect = self.vForm.frame;
aRect.size.height -= kbSize.height;
CGPoint scrollPoint = CGPointMake(0.0, self.activeField.frame.origin.y);
[self.scrollView setContentOffset:scrollPoint animated:YES];
}
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
self.vForm.contentInset = contentInsets;
self.vForm.scrollIndicatorInsets = contentInsets;
}
// This is where the magic happens. Set the class with this method as the UITextView''s delegate.
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
// Scroll the textview to the caret position
[textView scrollRangeToVisible:textView.selectedRange];
// Scroll the scrollview to the caret position within the textview
CGRect targetRect = [textView caretRectForPosition:textView.selectedTextRange.end];
targetRect.origin.y += self.activeField.frame.origin.y;
[self.scrollView scrollRectToVisible:targetRect animated:YES];
return YES;
}
Intenté incluir la mayoría del código de pegamento requerido. Las únicas cosas que faltan son configurar el delegado de UITextView y cerrar el teclado.
Tomó 2-3 días para averiguar lo que funcionaba anteriormente. Gracias apple
Todas las otras respuestas que intenté se comportaron de manera un tanto extraña para mí. El uso de un NSTimer
para realizar el desplazamiento también significaba que el usuario no podía desplazarse hacia arriba, ya que el caret terminaría fuera de la pantalla e inmediatamente se desplazaría hacia abajo nuevamente. Al final me quedé con el enfoque original de cambiar el marco de UITextView
en los eventos de notificación del teclado, y luego agregué los siguientes métodos:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
// Whenever the user enters text, see if we need to scroll to keep the caret on screen
[self scrollCaretToVisible];
return YES;
}
- (void)scrollCaretToVisible
{
//This is where the cursor is at.
CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end];
// Convert into the correct coordinate system
caretRect = [self.view convertRect:caretRect fromView:self.textView];
if(CGRectEqualToRect(caretRect, _oldRect)) {
// No change
return;
}
_oldRect = caretRect;
//This is the visible rect of the textview.
CGRect visibleRect = self.textView.frame;
//We will scroll only if the caret falls outside of the visible rect.
if (!CGRectContainsRect(visibleRect, caretRect))
{
// Work out how much the scroll position would have to change by to make the cursor visible
CGFloat diff = (caretRect.origin.y + caretRect.size.height) - (visibleRect.origin.y + visibleRect.size.height);
// If diff < 0 then this isn''t to do with the iOS7 bug, so ignore
if (diff > 0) {
// Scroll just enough to bring the cursor back into view
CGPoint newOffset = self.textView.contentOffset;
newOffset.y += diff;
[self.textView setContentOffset:newOffset animated:YES];
}
}
}
Funciona como un encanto para mí
Una solución más sencilla para este problema es actualizar el marco de vista de texto en respuesta al método de delegado textViewDidBegingEditing. Para más detalles, ver lo siguiente:
Cómo cambiar el tamaño de UITextView cuando se muestra el teclado con iOS 7