How to make a UITextView scroll while typing/editing

20,261

Solution 1

Problems with other answers:

  • when only scanning for "\n", if you type a line of text that exceeds the width of the text view, then scrolling will not occur.
  • when always setting contentOffset in textViewDidChange:, if you edit the middle of the text you do not want to scroll to the bottom.

The solution is to add this to the text view delegate:

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

Solution 2

I tried to put in your textViewDidChange: a snippet like:

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

It's not really clean, I'm working toward finding some better stuff, but for now it works :D

UPDATE: Since this is a bug that only happens on iOS 7 (Beta 5, for now), you can do a workaround with this code:

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

Then, on iOS 6 you can choose either to set the delay to 0.0 or to use just the content of the block.

Solution 3

Using 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)
        })
    }

Solution 4

I used the following code in the textViewDidChange: method and it seemed to work well.

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

This seems to scroll the UITextView slightly further so that your cursor isn't cut off.

Solution 5

Accepted answer when using Xamarin/Monotouch will look like

        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);
                    });
            }
        };
Share:
20,261

Related videos on Youtube

chrs
Author by

chrs

I'm just another programmer

Updated on July 09, 2022

Comments

  • chrs
    chrs almost 2 years

    UPDATE This seemed to be an issue with IOS 7 only. A great workaround has been added to accepted answer.

    I have created a custom control that contains a UITextView and UILabel which contains the title of the textview ie my control. My control automatically changes size to adapt the textview and the title. Before this happens I change the size of the textview to fit the text. This works optimally.

    I've added functionality so the textview automatically scrolls to the last line. Or that's at least what I'm trying. It works fine as long as the last line contains anything but empty text. If the text is empty, it rolls down so you can only see about half of the cursor.

    What am I doing wrong?

    So you can understand it better I have made some images:

    This is me typing a word and making some linebreaks. (Still not enough to make it scroll)

    Before making a line break

    And the I make a line break. (pressing enter) Look close at how the cursor is halved. This is the issue!

    The Issue

    I have made the next picture so you can see exactly what I expected.

    What I Want!

    • Vik
      Vik almost 11 years
      can you upload your simple Xcode project somewhere?
    • chrs
      chrs almost 11 years
      Yes, one sec, uploading to github
    • chrs
      chrs almost 11 years
      Source code + project, added to the question
    • Vik
      Vik almost 11 years
      Thanks, I added an answer
    • titaniumdecoy
      titaniumdecoy over 10 years
      It appears that this is still an issue in iOS 7.0.3.
  • chrs
    chrs almost 11 years
    I have no problem resizing the text view.
  • JustAnotherCoder
    JustAnotherCoder almost 11 years
    Oh I misunderstood your question. You just want to correct the error where only half the cursor is visible if the last line is empty correct?
  • chrs
    chrs almost 11 years
    True - Did you see the video?
  • chrs
    chrs almost 11 years
    I don't know why. But it didn't work for me :( Updated my Post
  • chrs
    chrs almost 11 years
    It works, but it is as you mention a little unclean. But Thanks for the effort. First "solution" at this time
  • Vik
    Vik almost 11 years
    Another thing you can do, maybe "cleaner" is: 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]; }); }
  • Vik
    Vik almost 11 years
    the problem is that in textViewDidChange: the contentSize is not yet updated to the real one, but to the one of the previous edit. It can also be a temporary bug with iOS 7
  • chrs
    chrs almost 11 years
    The last comment is also what I think for now, since everybody else thinks I am completely lost hehe. I am trying to download the 6.1 simulator and see if it runs on that smoothly.
  • Vik
    Vik almost 11 years
    I've just tried it on iOS 6.1 and it works ok, even setting the delay to 0.0
  • chrs
    chrs almost 11 years
    The "normal" approach or the hack?
  • Vik
    Vik almost 11 years
    The "normal" approach and the "hack"
  • chrs
    chrs almost 11 years
    OK. Can you update your answer so it is a bug, and you have found a workaround? Then I'll give you the bounty ;)
  • GoldenJoe
    GoldenJoe over 10 years
    Nice to see an actual workaround for this bug. How did Apple miss this??
  • tyler
    tyler over 10 years
    This didn't seem to do anything for me.
  • titaniumdecoy
    titaniumdecoy over 10 years
    This solution causes the text view to scroll a second time past the end after pasting a large block of text. @davidisdk's solution does not appear to have this issue.
  • LpLrich
    LpLrich over 10 years
    This made the UITextView hidden behind my keyboard and sort of defeated the whole purpose. I can imagine this working alongside something to compensate for hidden keyboard OR if the UITextView is at the top of the screen and therefore not likely to be hidden.
  • VIGNESH
    VIGNESH about 10 years
    The solves the problem with the last line, but causes strange scrolling when trying to edit near the top of a long document.
  • Travis
    Travis about 10 years
    There is a problem with this solution. 1. Add enough rows of text to fill text view. 2. Leave cursor on blank line on last row. 3. Scroll to top via touch. 4. Type a character. Bug: The content offset is too large. Expected: To be sexy as in other cases.
  • clopex
    clopex over 4 years
    For Swift 4 func textViewDidChange(_ textView: UITextView) { switch textView { case reasonsText: let bottomOffset = CGPoint(x: 0, y: textView.contentSize.height - textView.bounds.size.height) reasonsText.setContentOffset(bottomOffset, animated: false) default: break } }