Как сделать прокрутку UITextView при вводе/редактировании
UPDATE
Это, похоже, было проблемой только для IOS 7. В принятый ответ добавлено большое обходное решение.
Я создал настраиваемый элемент управления, который содержит UITextView и UILabel, который содержит заголовок textview, т.е. мой элемент управления.
Мой элемент управления автоматически изменяет размер, чтобы адаптировать текст и заголовок. Прежде чем это произойдет, я изменяю размер текстового поля в соответствии с текстом.
Это работает оптимально.
Я добавил функциональность, чтобы текст автоматически прокручивался до последней строки. Или это, по крайней мере, то, что я пытаюсь. Он отлично работает, пока последняя строка содержит ничего, кроме пустого текста. Если текст пуст, он скатывается вниз, поэтому вы можете видеть только половину курсора.
Что я делаю неправильно?
Итак, вы можете понять это лучше, я сделал несколько изображений:
Это я набираю слово и делаю несколько строк. (Все еще недостаточно, чтобы прокручивать)
![Before making a line break]()
И я делаю разрыв строки. (нажмите enter) Посмотрите, как уменьшен курсор. Это проблема!
![The Issue]()
Я сделал следующую картинку, чтобы вы могли точно видеть, что я ожидал.
![What I Want!]()
Ответы
Ответ 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
(конечно, с проверкой длины выделения).