54
votes

UITextView in iOS7 has been really weird. As you type and are entering the last line of your UITextView, the scroll view doesn't scroll to the bottom like it should and it causes the text to be "clipped". I've tried setting it's clipsToBound property to NO but it still clips the text.

I don't want to call on "setContentOffset:animated" because for one: that's very hacky solution.. secondly: if the cursor was in the middle (vertically) of our textview, it'll cause unwanted scrolling.

Here's a screenshot.

enter image description here

Any help would be greatly appreciated!

Thanks!

11
I'm experiencing the same issue on the device as well. Did you find any fix for this issue? Thanks!Peter Robert
Potential duplicate. My answer is here: stackoverflow.com/a/19200023/4397andygeers
Sorry for the late response -- I gave up on finding a solution and ended building out my "text view" with an embedded web view using the textarea element. Not my ideal solution but it works :T.ryan
For me, the issue still persists in iOS 7.0.3 on both the simulator and a device.bilobatum

11 Answers

93
votes

The problem is due to iOS 7. In the text view delegate, add this code:

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

The solution I found here was to add a one line fix after you create a UITextView:

self.textview.layoutManager.allowsNonContiguousLayout = NO;

This one line fixed three issues I had creating a UITextView-based code editor with syntax highlighting on iOS7:

  1. Scrolling to keep text in view when editing (the issue of this post)
  2. UITextView occasionally jumping around after dismissing the keyboard
  3. UITextView random scrolling jumps when trying to scroll the view

Note, I did resize the whole UITextView when the keyboard is shown/hidden.

6
votes

Try implementing the -textViewDidChangeSelection: delegate method from the UITextViewDelegate like this:

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

Heres a modified version of the selected answer by davidisdk.

- (void)textViewDidChange:(UITextView *)textView {
    NSRange selection = textView.selectedRange;

    if (selection.location + selection.length == [textView.text length]) {
        CGRect caretRect = [textView caretRectForPosition:textView.selectedTextRange.start];
        CGFloat overflow = caretRect.origin.y + caretRect.size.height - (textView.contentOffset.y + textView.bounds.size.height - textView.contentInset.bottom - textView.contentInset.top);

        if (overflow > 0.0f) {
            CGPoint offset = textView.contentOffset;
            offset.y += overflow + 7.0f;

            [UIView animateWithDuration:0.2f animations:^{
                [textView setContentOffset:offset];
            }];
        }
    } else {
        [textView scrollRangeToVisible:selection];
    }
}

I was getting a bug that when the textView's content size is larger then the bounds and the cursor is offscreen (such as using a keyboard and pressing the arrow key) the textView wouldn't animate to the text being inserted.

1
votes

Imho this is the definitive answer for all of the typical UITextView-scrolling / keyboard related issues in iOS 7. Its clean, its easy to read, easy to use, easy to maintain and can easily be reused.

The basic trick: Simply change the size of the UITextView, not the content inset!

Here's a hands-on example. It takes for granted that you have a NIB/Storyboard-based UIViewController using Autolayout and the UITextView fills out the entire root view in the UIViewController. If not you will have to adapt how you change the textViewBottomSpaceConstraint to your needs.

How to:


Add these properties:

@property (nonatomic, weak) IBOutlet NSLayoutConstraint *textViewBottomSpaceConstraint;
@property (nonatomic) CGFloat textViewBottomSpaceConstraintFromNIB;

Connect the textViewBottomSpaceConstraint in Interface Builder (dont forget!)

Then in viewDidLoad:

// Save the state of the UITextView's bottom constraint as set up in your NIB/Storyboard
self.textViewBottomSpaceConstraintFromNIB = self.textViewBottomSpaceConstraint.constant;

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillShowNotification:)
                                             name:UIKeyboardWillShowNotification
                                           object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillHideNotification:)
                                             name:UIKeyboardWillHideNotification
                                           object:nil];

Add these methods to handle Keyboard resizing (thanks to https://github.com/brennanMKE/Interfaces/tree/master/Keyboarding - these methods are by brennan!):

- (void)keyboardWillShowNotification:(NSNotification *)notification {
    CGFloat height = [self getKeyboardHeight:notification forBeginning:TRUE];
    NSTimeInterval duration = [self getDuration:notification];
    UIViewAnimationOptions curve = [self getAnimationCurve:notification];

    [self keyboardWillShowWithHeight:height duration:duration curve:curve];
}

- (void)keyboardWillHideNotification:(NSNotification *)notification {
    CGFloat height = [self getKeyboardHeight:notification forBeginning:FALSE];
    NSTimeInterval duration = [self getDuration:notification];
    UIViewAnimationOptions curve = [self getAnimationCurve:notification];

    [self keyboardWillHideWithHeight:height duration:duration curve:curve];
}

- (NSTimeInterval)getDuration:(NSNotification *)notification {
    NSDictionary *info = [notification userInfo];

    NSTimeInterval duration;

    NSValue *durationValue = [info objectForKey:UIKeyboardAnimationDurationUserInfoKey];
    [durationValue getValue:&duration];

    return duration;
}

- (CGFloat)getKeyboardHeight:(NSNotification *)notification forBeginning:(BOOL)forBeginning {
    NSDictionary *info = [notification userInfo];

    CGFloat keyboardHeight;

    NSValue *boundsValue = nil;
    if (forBeginning) {
        boundsValue = [info valueForKey:UIKeyboardFrameBeginUserInfoKey];
    }
    else {
        boundsValue = [info valueForKey:UIKeyboardFrameEndUserInfoKey];
    }

    UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation];
    if (UIDeviceOrientationIsLandscape(orientation)) {
        keyboardHeight = [boundsValue CGRectValue].size.width;
    }
    else {
        keyboardHeight = [boundsValue CGRectValue].size.height;
    }

    return keyboardHeight;
}

- (UIViewAnimationOptions)getAnimationCurve:(NSNotification *)notification {
    UIViewAnimationCurve curve = [[notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];

    switch (curve) {
        case UIViewAnimationCurveEaseInOut:
            return UIViewAnimationOptionCurveEaseInOut;
            break;
        case UIViewAnimationCurveEaseIn:
            return UIViewAnimationOptionCurveEaseIn;
            break;
        case UIViewAnimationCurveEaseOut:
            return UIViewAnimationOptionCurveEaseOut;
            break;
        case UIViewAnimationCurveLinear:
            return UIViewAnimationOptionCurveLinear;
            break;
    }

    return kNilOptions;
}

Finally, add these methods for reacting to the keyboard notifications and resize the UITextView

- (void)keyboardWillShowWithHeight:(CGFloat)height duration:(CGFloat)duration curve:(UIViewAnimationOptions)curve
{
    CGFloat correctionMargin = 15; // you can experiment with this margin so the bottom text view line is not flush against the keyboard which doesn't look nice
    self.textViewBottomSpaceConstraint.constant = height + correctionMargin;

    [self.view setNeedsUpdateConstraints];

    [UIView animateWithDuration:duration delay:0 options:curve animations:^{
        [self.view layoutIfNeeded];
    } completion:^(BOOL finished) {

    }];
}

- (void)keyboardWillHideWithHeight:(CGFloat)height duration:(CGFloat)duration curve:(UIViewAnimationOptions)curve
{
    self.textViewBottomSpaceConstraint.constant = self.textViewBottomSpaceConstraintFromNIB;

    [self.view setNeedsUpdateConstraints];

    [UIView animateWithDuration:duration delay:0 options:curve animations:^{
        [self.view layoutIfNeeded];
    } completion:^(BOOL finished) {

    }];
}

Also add these methods to automatically scroll to where the user clicked

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

- (void)textViewDidChangeSelection:(UITextView *)textView
{
    [textView scrollRangeToVisible:textView.selectedRange];
}
0
votes
textView.contentInset = UIEdgeInsetsMake(0.0, 0.0, 10.0, 0.0);

This will also address your issue

0
votes

If you are using StoryBoard then this behavior can also happen if you left AutoLayout on (as it is by default) and did not set top/bottom constraints for your UITextView. Check the File Inspector to see what your AutoLayout status is...

0
votes

Here is the MonoTouch version of davididsk's most excellent solution (from above).

TextView.SelectionChanged += (object sender, EventArgs e) => {
                TextView.ScrollRangeToVisible(TextView.SelectedRange);
            };


            TextView.Changed += (object sender, EventArgs e) => {

                CGRect line = TextView.GetCaretRectForPosition(TextView.SelectedTextRange.Start);
                nfloat overflow = line.Y + line.Height - 
                                     (TextView.ContentOffset.Y + 
                                      TextView.Bounds.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.Animate(0.1,()=> {
                        TextView.ContentOffset = offset;
                    });
                }
            };
0
votes

This line causes the last line of text to not show up for me:

textView.scrollEnabled = false

Try removing this and see what happens...

-1
votes
   textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

This resolved the issue for me

-2
votes

Set theViewDelegate to "self" in your .m and use in your .h then add this code to your .m

Will handle BOTH the versions of this glitch that are occurring for going to the next line with text (wrapping or carriage return) and typing... AND going to the next line with just a carriage return and no typing (this code, unlike other's code, will scroll to show the blinking cursor not being clipped in this second glitch scenario)

//!!!*!!****!*!**!*!*!!!MAKE SURE YOU SET DELEGATE AND USE THE <UITEXTVIEWDELEGATE>

-(void)textViewDidChange:(UITextView *)textView {
    [theTextView scrollRangeToVisible:[textView selectedRange]];//resizing textView frame causes text itself "content frame?" to still go below the textview frame and get clipped... auto-scrolling must be implimented. (iOS7 bug)
}

-(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
    if (([text isEqualToString:@"\n"]) && (range.location == textView.text.length)) {//"return" at end of textView
        [textView scrollRectToVisible:CGRectMake(5,5,5,999999999999999999) animated:NO];//for some reason the textViewDidChange auto scrolling doesnt work with a carriage return at the end of your textView... so I manually set it INSANELY low (NOT ANIMATED) here so that it automagically bounces back to the proper position before interface refreshes when textViewDidChange is called after this.
    }
    return YES;
}