Подтвердить что ты не робот

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);
}
4b9b3361

Ответ 1

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

Для получения дополнительной информации о 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 должна обеспечить лучший способ обработки каретки, но это работает.

Ответ 2

Все остальные ответы, которые я попробовал, вели себя несколько странно для меня. Использование 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];
        }
    }
}

Работает как очарование для меня

Ответ 3

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

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

Ответ 4

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

В .h

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

В .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
    }
}

Ответ 5

Это то, что я закончил, и что-то похожее на работу:

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

Ответ 7

Для тех, у кого есть UITextView внутри a 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 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 дня, чтобы выяснить, что раньше работало. Спасибо, Apple.

Ответ 8

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

При переключении клавиатур вызывается 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;
 }];

Ответ 9

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

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

}