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

Как сделать прокрутку UITextView при вводе/редактировании

UPDATE Это, похоже, было проблемой только для IOS 7. В принятый ответ добавлено большое обходное решение.

Я создал настраиваемый элемент управления, который содержит UITextView и UILabel, который содержит заголовок textview, т.е. мой элемент управления. Мой элемент управления автоматически изменяет размер, чтобы адаптировать текст и заголовок. Прежде чем это произойдет, я изменяю размер текстового поля в соответствии с текстом. Это работает оптимально.

Я добавил функциональность, чтобы текст автоматически прокручивался до последней строки. Или это, по крайней мере, то, что я пытаюсь. Он отлично работает, пока последняя строка содержит ничего, кроме пустого текста. Если текст пуст, он скатывается вниз, поэтому вы можете видеть только половину курсора.

Что я делаю неправильно?

Итак, вы можете понять это лучше, я сделал несколько изображений:

Это я набираю слово и делаю несколько строк. (Все еще недостаточно, чтобы прокручивать)

Before making a line break

И я делаю разрыв строки. (нажмите enter) Посмотрите, как уменьшен курсор. Это проблема!

The Issue

Я сделал следующую картинку, чтобы вы могли точно видеть, что я ожидал.

What I Want!

4b9b3361

Ответ 1

Проблемы с другими ответами:

  • при сканировании только для "\n", если вы набираете строку текста, которая превышает ширину текстового вида, прокрутка не будет выполняться.
  • когда вы всегда устанавливаете contentOffset в textViewDidChange:, если вы редактируете середину текста, который вы не хотите прокручивать донизу.

Решение состоит в том, чтобы добавить это в делегат текстового представления:

- (void)textViewDidChange:(UITextView *)textView {
    CGRect line = [textView caretRectForPosition:
        textView.selectedTextRange.start];
    CGFloat overflow = line.origin.y + line.size.height
        - ( textView.contentOffset.y + textView.bounds.size.height
        - textView.contentInset.bottom - textView.contentInset.top );
    if ( overflow > 0 ) {
        // We are at the bottom of the visible text and introduced a line feed, scroll down (iOS 7 does not do it)
        // Scroll caret to visible area
        CGPoint offset = textView.contentOffset;
        offset.y += overflow + 7; // leave 7 pixels margin
        // Cannot animate with setContentOffset:animated: or caret will not appear
        [UIView animateWithDuration:.2 animations:^{
            [textView setContentOffset:offset];
        }];
    }
}

Ответ 2

Я попытался добавить в свой textViewDidChange: фрагмент, например:

if([textView.text hasSuffix:@"\n"])
    [self.textView setContentOffset:CGPointMake(0,INT_MAX) animated:YES];

Это не очень чисто, я работаю над поиском лучшего материала, но на данный момент он работает: D

UPDATE: Поскольку это ошибка, которая происходит только на iOS 7 (Beta 5, пока), вы можете сделать обходной путь с этим кодом:

if([textView.text hasSuffix:@"\n"]) { 
    double delayInSeconds = 0.2; 
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); 
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){ 
        CGPoint bottomOffset = CGPointMake(0, self.textView.contentSize.height - self.textView.bounds.size.height); 
        [self.textView setContentOffset:bottomOffset animated:YES]; 
    }); 
}

Затем на iOS 6 вы можете либо установить задержку на 0.0, либо использовать только содержимое блока.

Ответ 3

Использование Swift 3: -

let line : CGRect = textView.caretRect(for: (textView.selectedTextRange?.start)!)
    print("line = \(line)")

    let overFlow = line.origin.y + line.size.height - (textView.contentOffset.y + textView.bounds.size.height - textView.contentInset.bottom - textView.contentInset.top)

    print("\n OverFlow = \(overFlow)")

    if (0 < overFlow)
    {
        // We are at the bottom of the visible text and introduced a line feed, scroll down (iOS 7 does not do it)
        // Scroll caret to visible area

        var offSet : CGPoint = textView.contentOffset

        print("offSet = \(offSet)")

        //leave 7 pixels margin
        offSet.y += (overFlow + 7)

        //Cannot animate with setContentOffset:animated: or caret will not appear

        UIView.animate(withDuration: 0.3, animations: {
            textView.setContentOffset(offSet, animated: true)
        })
    }

Ответ 4

Я использовал следующий код в методе textViewDidChange:, и он, казалось, работал хорошо.

- (void)textViewDidChange:(UITextView *)textView {
    CGPoint bottomOffset = CGPointMake(0, self.theTextView.contentSize.height - self.theTextView.bounds.size.height);
    [self.theTextView setContentOffset:bottomOffset animated:YES];
}

Кажется, что прокрутка UITextView немного дальше, чтобы ваш курсор не был отключен.

Ответ 5

Принятый ответ при использовании Xamarin/Monotouch будет выглядеть как

        textView.Changed += (object sender, EventArgs e) =>
        {

            var line = textView.GetCaretRectForPosition(textView.SelectedTextRange.start);
            var overflow = line.Top + line.Height -
                           (textView.ContentOffset.Y
                           + textView.Bounds.Size.Height
                           - textView.ContentInset.Bottom
                           - textView.ContentInset.Top);
            if (overflow > 0)
            {
                var offset = textView.ContentOffset;
                offset = new PointF(offset.X, offset.Y + overflow + 7);
                UIView.Animate(0.2f, () =>
                    {
                        textView.SetContentOffset(offset, false);
                    });
            }
        };

Ответ 6

Следующая модификация ответа Вика отлично справилась со мной:

if([_textView.text hasSuffix:@"\n"])
{
    if (_textView.contentSize.height - _textView.bounds.size.height > -30)
    {
        double delayInSeconds = 0.2;
        dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
        dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
        {
            CGPoint bottomOffset = CGPointMake(0, _textView.contentSize.height - _textView.bounds.size.height);
            [_textView setContentOffset:bottomOffset animated:YES];
        });
    }
}

Ответ 7

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

[self.textView.layoutManager ensureLayoutForTextContainer:self.textView.textContainer];

Ответ 8

Кто-нибудь подал ошибку в яблоке в связи с этой проблемой? Это похоже на довольно очевидную ошибку, которую очень легко воспроизвести. Если никто не ответит, я отправлю радар с тестовым проектом.

Ответ 9

Я думаю, что лучший способ - определить фактическую позицию курсора, чтобы увидеть, требуется ли прокрутка.

- (void)textViewDidChange:(UITextView *)textView {
    // check to see if the cursor is at the end of the text
    if (textView.text.length == textView.selectedRange.location) {
        // find the caret position
        CGRect caret = [textView caretRectForPosition:textView.selectedTextRange.start];

        // determine the height of the visible text window
        UIEdgeInsets textInsets = textView.textContainerInset;
        CGFloat textViewHeight = textView.frame.size.height - textInsets.top - textInsets.bottom;
        // need to subtract the textViewHeight to correctly get the offset
        // that represents the top of the text window above the cursor
        textView.contentOffset = CGPointMake(textView.contentOffset.x, caret.origin.y - textViewHeight);
    }
}

Вышеприведенный код определит, находится ли каретка в конце текста. Если это не так, он не будет прокручиваться. Если это (независимо от последнего символа), он будет определять правильное смещение для прокрутки и выполнения прокрутки.

Ответ 10

Это то, что я использовал в своем текущем проекте для изменения размера UITextView:

- (void)textViewDidChange:(UITextView *)textView {
    CGRect frame = textView.frame;
    frame.size.height = textView.contentSize.height;
    textView.frame = frame;    
}

Это работает очень хорошо для меня. Если вы хотите создать небольшую "границу" между курсором и фактическим текстовым полем, вы всегда можете добавить несколько пикселей к высоте. Например:

    frame.size.height = textView.contentSize.height+14;

Ответ 11

Решение в принятом ответе непригодно.

Скажем, что в textView есть 1000 слов, а последний символ -\n. Если вы отредактируете первую строку textView, hasSuffix:@"\n" вернет YES, и textView немедленно перейдет к нижней части документа.

Или, начните с пустой textView и введите одно слово, затем нажмите return. Текст будет прокручиваться до нижней части.

============  ============   ============   ============
 Te|           Text |         Text           
                              |


                                             Text
                                             |
============  ============   ============   ============

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

-(void)textViewDidChange:(UITextView *)textView {

    // Get caret frame
    UITextPosition *caret = [textView positionFromPosition:textView.beginningOfDocument offset:textView.selectedRange.location];
    CGRect caretFrame     = [textView caretRectForPosition:caret];

    // Get absolute y position of caret in textView
    float absCaretY       = caretFrame.origin.y - textView.contentOffset.y;

    // Set a max y for the caret (in this case the textView is resized to avoid the keyboard and an arbitrary padding is added)
    float maxCaretY       = textView.frame.size.height - 70;

    // Get how far below the maxY the caret is
    float overflow        = absCaretY - maxCaretY;

    // No need to scroll if the caret is above the maxY
    if (overflow < 0)
        return;

    // Need to add a delay for this to work
    double delayInSeconds = 0.2;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){

        // Scroll to the maxCaretY
        CGPoint contentOffset = CGPointMake(0, textView.contentOffset.y + overflow);
        [textView setContentOffset:contentOffset animated:YES];
    });
}

Ответ 12

Попробуйте использовать

   textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
   textView.autoresizingSubviews = YES;

Он решил проблему для меня для iOS7.

Ответ 13

На iOS10 в моем автоусилении UITextView ключ для меня был

// my method called on text change

- (void)updateLayout {

    [self invalidateIntrinsicContentSize];

    [UIView animateWithDuration:0.33 animations:^{

        [self.superview layoutIfNeeded];

        CGPoint bottomOffset = CGPointMake(0, self.contentSize.height - self.bounds.size.height);
        [self setContentOffset:bottomOffset animated:NO];

    } completion:nil];

}

Весь класс

#import "AutosizeTextView.h"

@implementation AutosizeTextView

- (instancetype)initWithFrame:(CGRect)frame {

    if (self = [super initWithFrame:frame]) {
        [self setup];
    }
    return self;
}

- (void)awakeFromNib {

    [super awakeFromNib];

    [self setup];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidChangeNotification object:self];
}

- (void)setText:(NSString *)text {
    [super setText:text];
    [self updateLayout];
}

- (CGSize)intrinsicContentSize {
    CGRect textRect = [self.layoutManager usedRectForTextContainer:self.textContainer];
    CGFloat height = textRect.size.height + self.textContainerInset.top + self.textContainerInset.bottom;
    return CGSizeMake(UIViewNoIntrinsicMetric, height);
}


////////////////////////////////////////////////////////////////////////
#pragma mark - Private
////////////////////////////////////////////////////////////////////////

- (void)setup {

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textDidChangeNotification:) name:UITextViewTextDidChangeNotification object:self];
    self.textContainer.lineFragmentPadding = 0;
    self.textContainerInset = UIEdgeInsetsMake(4, 4, 4, 4);

}

- (void)updateLayout {

    [self invalidateIntrinsicContentSize];

    [UIView animateWithDuration:0.33 animations:^{

        [self.superview layoutIfNeeded];

        CGPoint bottomOffset = CGPointMake(0, self.contentSize.height - self.bounds.size.height);
        [self setContentOffset:bottomOffset animated:NO];

    } completion:nil];

}

////////////////////////////////////////////////////////////////////////
#pragma mark - Notification
////////////////////////////////////////////////////////////////////////

- (void)textDidChangeNotification:(NSNotification *)notification {

    [self updateLayout];

}

@end

Ответ 14

В Swift 3

введите описание изображения здесь

Установить ссылочную точку и делегат textview

class ViewController: UIViewController , UITextViewDelegate{

@IBOutlet var txtViewRef: UITextView!

В представлении setDidLoad и уведомлении об изменении KeyboardFrame или Скрытии клавиатуры

 override func viewDidLoad() {
    super.viewDidLoad()

    txtViewRef.delegate = self
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.updateTextView(notification:)), name: Notification.Name.UIKeyboardWillChangeFrame, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.updateTextView(notification:)), name: Notification.Name.UIKeyboardWillHide, object: nil)    
}

Создать функцию updateTextView. В которой мы получаем рамку клавиатуры и меняем вставку содержимого и индикатор прокрутки и прокручиваем текст

func updateTextView(notification : Notification)
{
    let userInfo = notification.userInfo!
    let keyboardEndFrameScreenCoordinates = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
    let keyboardEndFrame = self.view.convert(keyboardEndFrameScreenCoordinates, to: view.window)

    if notification.name == Notification.Name.UIKeyboardWillHide{
        txtViewRef.contentInset = UIEdgeInsets.zero
    }
    else
    {
        txtViewRef.contentInset = UIEdgeInsetsMake(0, 0, keyboardEndFrame.height, 0)
        txtViewRef.scrollIndicatorInsets = txtViewRef.contentInset
    }

    txtViewRef.scrollRangeToVisible(txtViewRef.selectedRange)

}

Ответ 15

У меня была та же проблема, но с UITextView в UITableView, поэтому после некоторого исследования я не нашел "простого" способа исправить это, поэтому на основе принятого ответа я создал идеально работающее решение (должно работать также внутри UICollectionView, UIScrollView с некоторыми изменениями, прокомментированными внутри этого расширения).

Так что для легкого повторного использования необходимо несколько расширений поверх UIKit:

extension UITextView {

    func scrollToCursor(animated: Bool = false, verticalInset: CGFloat = 8) {
        guard let selectedTextRange = selectedTextRange else { return }
        var cursorRect = caretRect(for: selectedTextRange.start)

        // NOTE: can't point UIScrollView, coz on iOS 10 closest view will be UITableWrapperView
        // to extend functionality for UICollectionView or plain UIScrollView it better to search them one by one
        let scrollView = findParent(of: UITableView.self) ?? self
        cursorRect = convert(cursorRect, to: scrollView)

        if cursorRect.origin.x.isInfinite || cursorRect.origin.y.isInfinite {
            return
        }

        let bottomOverflow = cursorRect.maxY - (scrollView.contentOffset.y + scrollView.bounds.height - scrollView.contentInset.bottom - scrollView.contentInset.top)

        if bottomOverflow > 0 {
            let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y + bottomOverflow + verticalInset)
            scrollView.setContentOffset(offset, animated: animated)
            return
        }

        let topOverflow = scrollView.contentOffset.y - cursorRect.minY
        if topOverflow > 0 {
            let offset = CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y - topOverflow - verticalInset)
            scrollView.setContentOffset(offset, animated: animated)
        }
    }
}

UIView:

extension UIView {
    func findParent<Parent: UIView>(of parentType: Parent.Type) -> Parent? {
        return superview?.findNext(of: parentType)
    }

    private func findNext<Parent: UIView>(of parentType: Parent.Type) -> Parent? {
        if let res = self as? Parent {
            return res
        }

        return superview?.findNext(of: parentType)
    }
}

Поэтому в UITextViewDelegate, когда текст изменяется, звоните туда, где вам нужно (может быть внутри основного асинхронного блока очереди отправки - для этого я использую обратный вызов ReactiveSwift):

textView.scrollToCursor()

Если вы хотите добавить перемещение вверх при изменении позиции курсора (в верхней части экрана), необходимо вызвать этот метод внутри textViewDidChangeSelection делегата textViewDidChangeSelection (конечно, с проверкой длины выделения).