it-swarm.com.ru

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

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

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

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

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

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

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

Before making a line break

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

The Issue

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

What I Want!

27
chrs

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

  • при сканировании только на «\ 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];
        }];
    }
}
55
davidisdk

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

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

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

UPDATE: Так как эта ошибка возникает только в iOS 7 (пока бета-версия 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, либо использовать только содержимое блока.

8
Vik

Используя 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
kishor0011

Принятый ответ при использовании 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);
                    });
            }
        };
3
Alex Sorokoletov

Я использовал следующий код в методе 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 немного дальше, чтобы ваш курсор не обрезался.

3
hgwhittle

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

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];
        });
    }
}
2
shersa1986

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

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

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

1
mikeho

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

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

1
tharris

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

1
tyler

У меня была та же проблема, но насчет UITextView inside 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's 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 (с проверкой длины выбора, конечно). 

0
HotJard

В Swift 3 

 enter image description here

Установить справочный выход и делегат textview

class ViewController: UIViewController , UITextViewDelegate{

@IBOutlet var txtViewRef: UITextView!

В viewDidLoad установите делегата и уведомление для изменения KeyboardFrame или Hide the клавиатуры 

 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)

}
0
Mili Shah

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

Скажем, в 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];
    });
}
0
nate

На 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
0
Peter Lapisu

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

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

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

0
Shilpi

Вот что я использовал в своем текущем проекте для изменения размера 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;
0
JustAnotherCoder