如何在输入/编辑时使 UITextView 滚动

Posted

技术标签:

【中文标题】如何在输入/编辑时使 UITextView 滚动【英文标题】:How to make a UITextView scroll while typing/editing 【发布时间】:2013-08-06 01:44:54 【问题描述】:

更新 这似乎只是 ios 7 的问题。已为接受的答案添加了一个很好的解决方法。

我创建了一个包含 UITextView 和 UILabel 的自定义控件,其中包含 textview 的标题,即我的控件。 我的控件会自动更改大小以适应文本视图和标题。在此之前,我更改了 textview 的大小以适合文本。 这样效果最佳。

我已经添加了功能,所以 textview 会自动滚动到最后一行。或者这至少是我正在尝试的。只要最后一行包含除空文本之外的任何内容,它就可以正常工作。如果文本为空,它会向下滚动,因此您只能看到大约一半的光标。

我做错了什么?

所以你可以更好地理解它我做了一些图像:

这是我输入一个单词并进行一些换行符。 (仍然不足以让它滚动)

然后我换行。 (按回车键)仔细观察光标是如何减半的。这就是问题所在!

我已经制作了下一张照片,所以你可以看到我所期望的。

【问题讨论】:

你能把你的简单 Xcode 项目上传到某个地方吗? 对,一秒,上传到github 源码+项目,添加到问题中 谢谢,我添加了答案 在 iOS 7.0.3 中这似乎仍然是一个问题。 【参考方案1】:

其他答案的问题:

仅扫描“\n”时,如果键入的文本行超出文本视图的宽度,则不会发生滚动。 当总是在 textViewDidChange: 中设置 contentOffset 时,如果您编辑文本的中间部分,您不想滚动到底部。

解决方案是将这个添加到文本视图委托中:

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

【讨论】:

解决了最后一行的问题,但在长文档顶部附近尝试编辑时会导致奇怪的滚动。 这个解决方案有问题。 1. 添加足够多的文本行来填充文本视图。 2. 将光标留在最后一行的空白行。 3. 通过触摸滚动到顶部。 4. 输入一个字符。 Bug:内容偏移量太大。预期:像其他情况一样性感。【参考方案2】:

我试图在你的textViewDidChange: 中添加一个类似的sn-p:

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

它不是很干净,我正在努力寻找更好的东西,但现在它可以工作了:D

更新: 由于这是仅在 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 或仅使用块的内容。

【讨论】:

它有效,但正如你提到的那样有点不干净。但感谢您的努力。此时的第一个“解决方案” 你可以做的另一件事,也许“更干净”是: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]; ); 问题在于textViewDidChange: 中的 contentSize 尚未更新为真实的,而是更新为先前编辑的。它也可能是 iOS 7 的临时错误 最后的评论也是我现在的想法,因为其他人都认为我完全迷路了嘿嘿。我正在尝试下载 6.1 模拟器,看看它是否能顺利运行。 我刚刚在 iOS 6.1 上尝试过,它工作正常,甚至将延迟设置为 0.0【参考方案3】:

我在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,这样你的光标就不会被切断。

【讨论】:

我不知道为什么。但这对我不起作用:(更新了我的帖子 对于 Swift 4 func textViewDidChange(_ textView: UITextView) switch textView case reasonText: let bottomOffset = CGPoint(x: 0, y: textView.contentSize.height - textView.bounds.size.height ) reasonText.setContentOffset(bottomOffset, animated: false) 默认值:break 【参考方案4】:

使用 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)
        )
    

【讨论】:

【参考方案5】:

Accepted answer 使用 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】:

以下对 Vik 答案的修改对我来说效果很好:

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】:

有没有人就这个问题向苹果提交过错误?这感觉就像一个非常明显的错误,很容易重现。如果没有人回应,那么我将提交一份带有测试项目的雷达。

【讨论】:

【参考方案8】:

我发现如果您在 viewWillAppear 中添加以下内容,它将解决这个问题以及 UITextView 在 beta 中出现的一些其他问题:

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

【讨论】:

这似乎对我没有任何作用。 这使得 UITextView 隐藏在我的键盘后面,有点破坏了整个目的。我可以想象这与补偿隐藏键盘的东西一起工作,或者如果 UITextView 位于屏幕顶部,因此不太可能被隐藏。【参考方案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】:

在 Swift 3 中

设置textview的引用出口和委托

class ViewController: UIViewController , UITextViewDelegate

@IBOutlet var txtViewRef: UITextView!

在 viewDidLoad 中设置委托和更改 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)


【讨论】:

【参考方案11】:

这是我在当前项目中用来调整 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;

【讨论】:

调整文本视图的大小没有问题。 哦,我误解了你的问题。如果最后一行为空正确,您只想更正只有一半光标可见的错误? 正确 - 你看过视频吗?【参考方案12】:

已接受答案中的解决方案无法使用。

假设textView中有1000个单词,最后一个字符是“\n”。如果编辑 textView 的第一行,hasSuffix:@"\n" 将返回 YES,并且 textView 将立即滚动到文档底部。

或者,从一个空白的 textView 开始并输入一个单词,然后按回车键。文本将滚动到底部。

============  ============   ============   ============
 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];
    );

【讨论】:

此解决方案会导致文本视图在粘贴大块文本后第二次滚动到末尾。 @davidisdk 的解决方案似乎没有这个问题。【参考方案13】:

尝试使用

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

它为我解决了 iOS7 的问题。

【讨论】:

【参考方案14】:

在 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

【讨论】:

【参考方案15】:

我遇到了同样的问题,但是关于 UITableView 中的 UITextView,所以经过一番调查,我没有找到任何 “简单” 方法修复它,所以根据接受的答案我创建了完美的解决方案(应该也可以在 UICollectionView、UIScrollView strong> 在此扩展中注释了一些更改)。

所以为了方便重用需要在 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 委托的方法中调用此方法(当然要检查选择长度)。

【讨论】:

以上是关于如何在输入/编辑时使 UITextView 滚动的主要内容,如果未能解决你的问题,请参考以下文章

当键盘可见时如何使 UITextView 一直滚动到底部

xcode 6无法滚动长的uitextview

如何在(有时)可编辑的 UITextView 中启用数据检测器

当嵌套的 UITextView 开始编辑时,UICollectionView 跳转/滚动

UITextView如何将光标保持在键盘上方

如何使用 jQuery 在单击和鼠标悬停时使可滚动的 div 滚动