Monday, September 23, 2013

Last Line(s) of a UITextView May Scroll Past the Bounds of the View

I ran across this oddity today that rears its head in UITextViews under iOS 7 (didn't occur under iOS 6). Essentially, the view allows text to be written past the bottom of the UITextView's frame causing text to be hidden by a keyboard and/or keyboard accessory. This problem doesn't manifest if you're typing and the text word wraps; it only appears after tapping Return (whether after text or by itself). The following snippet (which goes in your UITextViewDelegate) did the trick for me:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    [textView scrollRangeToVisible:range];

    if ([text isEqualToString:@"\n"]) {
        [UIView animateWithDuration:0.2 animations:^{
            [textView setContentOffset:CGPointMake(textView.contentOffset.x, textView.contentOffset.y + 20)];
        }];
    }

    return YES;
}

The call to -scrollRangetoVisible mostly does the trick. When you start typing at the bottom of the UITextView, the text will be hidden, but will immediately scroll up to become visible. A touch odd, but bearable. However, if you just tap Return, the text will continue to be non-visible. Thus, we check for a newline, and if so, adjust the contentOffset for the view by an appropriate amount for your text (probably better to calculate the right size if you allow the user to choose a font and/or font size). The animation makes the adjustment smooth so the text view doesn't jerk each time text is changed.

Here's to hoping Apple gets this one fixed in the next point release or two. I'll be filing a Radar.

Update 2013-09-24

After working with the the original version some more, it had a number of shortcomings. This is the version I'm using now, which should be more robust and should properly account for font and font size changes.

- (BOOL)textView:(UITextView *)tView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    CGRect textRect = [tView.layoutManager usedRectForTextContainer:tView.textContainer];
    CGFloat sizeAdjustment = tView.font.lineHeight * [UIScreen mainScreen].scale;
    
    if (textRect.size.height >= tView.frame.size.height - sizeAdjustment) {
        if ([text isEqualToString:@"\n"]) {
            [UIView animateWithDuration:0.2 animations:^{
                [tView setContentOffset:CGPointMake(tView.contentOffset.x, tView.contentOffset.y + sizeAdjustment)];
            }];
        }
    }
    
    return YES;
}

Update 2013-10-03

Kevin Hayes has updated my latest update to account for UITextViews where the view extends underneath the keyboard. He's detailed his update here: Autoscrolling UITextView in iOS7.

Additionally, I've found adding this bit (from the UITextViewDelegate) also helps things work more smoothly, as well:

- (void)textViewDidChangeSelection:(UITextView *)textView {
    [textView scrollRangeToVisible:textView.selectedRange];
}