it-swarm.com.ru

UITextView курсор ниже кадра при смене кадра

У меня есть UIViewCOntroller, который содержит UITextView. Когда появляется клавиатура, я изменяю ее размер следующим образом:

#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;
}

Кажется, что textView изменяется до нужного размера, но когда пользователь печатает, курсор оказывается «вне» рамки textView. Смотрите картинку ниже:

enter image description here

Желтая область - это рамка UITextView (я не знаю, что такое синяя линия рядом с клавишей R). Я нахожу это довольно запутанным. Я использую iOS7, если это имеет какое-либо значение. 

Есть идеи или советы?

Обновление

У меня есть подкласс UITextView, который рисует горизонтальные линии с помощью следующего метода (если это имеет значение):

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

Вместо того, чтобы изменять размер фрейма, почему бы не дать вашему текстовому представлению contentInset (и соответствующий scrollIndicatorInsets)? Помните, что текстовые представления на самом деле представляют собой прокрутки. Это правильный способ справиться с клавиатурными (или другими) помехами.

Для получения дополнительной информации о contentInset см. это вопрос.


Кажется, этого недостаточно. По-прежнему используйте вставки, так как это более правильно (особенно на iOS7, где клавиатура прозрачная), но вам также потребуется дополнительная обработка для каретки:

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

Много работы, Apple должна предоставить лучший способ обработки каретки, но это работает.

20
Leo Natan

Все остальные ответы, которые я пробовал, вели себя странно для меня. Использование NSTimer для выполнения прокрутки также означало, что пользователь не мог прокручивать вверх, так как каретка тогда заканчивала бы за пределами экрана и сразу же снова прокручивалась вниз. В конце концов я остановился на оригинальном подходе к изменению фрейма UITextView в событиях уведомления клавиатуры, а затем добавил следующие методы:

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

Работает как шарм для меня

6
andygeers

Уже много ответов, я обнаружил, что в моем случае это на самом деле намного проще. On клавиатураWillShow Я настраиваю текстовое представление contentInset и сохраняю кадр в полноэкранном режиме. И хотя scrollRangeToVisible: не работает для меня, как и для многих других, методы представления прокрутки (от которых наследуется UITextView) работают просто отлично. Это работает для меня:

- (void)textViewDidChange:(UITextView *)textView
{
    CGRect caret = [_textView caretRectForPosition:_textView.selectedTextRange.end];
    [_textView scrollRectToVisible:caret animated:YES];
}
4
Pascal

Андерс и Лео Натан имеют отличные решения. Однако мне нужно было немного изменить их ответы, чтобы прокрутка работала корректно с contentInset. Проблема, с которой я столкнулся, заключалась в том, что textViewDidBeginEditing: вызывается раньше, чем keyboardWasShown:, поэтому изменение содержимого не отражается в первый раз. Вот что я сделал:

В .ч

@interface NoteDayViewController : UIViewController <UITextViewDelegate>
{
    UIEdgeInsets noteTextViewInsets;
    UIEdgeInsets noteTextViewScrollIndicatorInsets;
    CGRect oldRect;
    NSTimer *caretVisibilityTimer;
    float noteViewBottomInset;
}
@property (weak, nonatomic) IBOutlet UITextView *noteTextView;

В м

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

Для тех, у кого есть UITextView внутри UIScrollView, где iOS <7 позаботилась о прокрутке каретки: Вот как это работает с iOS 7 (а также 5 & 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;
}

Я попытался включить большую часть необходимого кода клея. Единственное, чего не хватает, это установить делегат UITextView и убрать клавиатуру.

Потребовалось 2-3 дня, чтобы выяснить, что раньше работало. Спасибо, Эппл.

1
domsom

Более простое решение этой проблемы - обновить рамку текстового представления в ответ на метод делегата textViewDidBegingEditing. Для получения дополнительной информации см. Следующее:

Как изменить размер UITextView, когда клавиатура показана с iOS 7

1
ColinE

Приведенный выше комментарий Ангела Найденова является верным, особенно в таких случаях, как переключение с английской на японскую клавиатуру, которая показывает подсказку. 

При переключении клавиатуры вызывается UIKeyboardWillShowNotification, но UIKeyboardWillHideNotification не вызывается.

Таким образом, вы должны настроить вкладку, чтобы использовать абсолютное значение, а не использовать +=.

Безотносительно, [self.textView setContentOffset:newOffset animated:YES]; фактически не изменит графику в iOS 7.1 после того, как клавиатура будет показана во второй раз, что, вероятно, является ошибкой. Обходной путь, который я использовал, заменяет

[self.textView setContentOffset:newOffset animated:YES]; 

с

[UIView animateWithDuration:.25 animations:^{
        self.textView.contentOffset = newOffset;
 }];
1
ykonda

Это то, что я в итоге сделал, и кое-что, что, кажется, работает:

- (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];
}
1
Anders

Лео Натан, ты хорошо начал, но твоя казнь была относительно неэффективной. Вот лучший способ сделать это с меньшим количеством кода:

// 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];

}
0
HomeGrown