54

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!

ryan
  • 1,324
  • 2
  • 18
  • 29
  • I'm experiencing the same issue on the device as well. Did you find any fix for this issue? Thanks! – Peter Robert Oct 02 '13 at 06:45
  • Potential duplicate. My answer is here: http://stackoverflow.com/a/19200023/4397 – andygeers Oct 05 '13 at 19:42
  • possible duplicate of [UITextView cursor below frame when changing frame](http://stackoverflow.com/questions/18431684/uitextview-cursor-below-frame-when-changing-frame) – andygeers Oct 05 '13 at 19:42
  • 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 Oct 16 '13 at 20:11
  • For me, the issue still persists in iOS 7.0.3 on both the simulator and a device. – bilobatum Oct 23 '13 at 22:38
  • There is no issue on 7.1 except the last line is empty. And all solutions don't work when the last line is empty. So there is no solution on iOS 7.1. – Dmitry Jun 08 '14 at 17:11
  • Does anybody have a fix for iOS 7.1? – Dmitry Jun 08 '14 at 17:13
  • The issue is FIXED on iOS 8. – Dmitry Jun 08 '14 at 17:24

11 Answers11

93

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];
        }];
    }
}
davidisdk
  • 3,358
  • 23
  • 12
  • 1
    At last a solution that works! I spent several hours on this issue until found this solution... Thanks a lot! BTW, any idea if this is a bug? I didn't have this issue on previous iOS versions. – Joshua Oct 09 '13 at 17:46
  • Thank you so much! I hope it will be fixed in future versions of iOS. – Evgenii Nov 24 '13 at 01:58
  • 1
    GREAT JOB MAN @ davidisdk – Dhaval H. Nena Dec 07 '13 at 07:45
  • It doesn't work on iOS 7.1 when the last line is empty. But when it's not empty there is no the issue at all. – Dmitry Jun 08 '14 at 17:11
  • @Altaveron : I was like you, nothing work on iOS 7.1, until I found this : http://stackoverflow.com/a/19797795/1511646 (hope it's help ;) – Dam Jun 09 '14 at 14:09
  • user0000000, doesn't help me. But the following helps: http://stackoverflow.com/questions/22315755/ios-7-1-uitextview-still-not-scrolling-to-cursor-caret-after-new-line/24108958#24108958 – Dmitry Jun 09 '14 at 15:29
  • Here is the MonoTouch version of davididsk's most excellent solution (see below). – aumansoftware Feb 24 '15 at 17:09
21

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.

gmyers
  • 211
  • 2
  • 4
6

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

-(void)textViewDidChangeSelection:(UITextView *)textView {
    [textView scrollRangeToVisible:textView.selectedRange];
}
chris
  • 16,324
  • 9
  • 37
  • 40
2

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.

cnotethegr8
  • 7,342
  • 8
  • 68
  • 104
  • It doesn't work on iOS 7.1 when the last line is empty. But when it's not empty there is no the issue at all. Do you have a fix for iOS 7.1? – Dmitry Jun 08 '14 at 17:12
1

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];
}
Wirsing
  • 6,713
  • 3
  • 15
  • 13
0
textView.contentInset = UIEdgeInsetsMake(0.0, 0.0, 10.0, 0.0);

This will also address your issue

Dimitris
  • 682
  • 3
  • 14
  • 19
0

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...

Fabrice
  • 11
  • 1
0

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;
                    });
                }
            };
aumansoftware
  • 845
  • 8
  • 6
0

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

textView.scrollEnabled = false

Try removing this and see what happens...

noobular
  • 3,257
  • 2
  • 24
  • 19
-1
   textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

This resolved the issue for me

Shilpi
  • 498
  • 1
  • 6
  • 13
-2

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;
}
Albert Renshaw
  • 17,282
  • 18
  • 107
  • 195
  • 2
    It's hard to take code seriously when it contains something like `CGRectMake(5,5,5,999999999999999999)`. – Steve Madsen Nov 20 '14 at 14:31
  • @SteveMadsen it's just `scrollToBottom`. I could have told him to import and instead used `DBL_MAX` but I figured that would be too much of a hassle so I just spammed 9's, it has the same effect. – Albert Renshaw Nov 20 '14 at 22:09