UITextView in iOS7 clips the last line of text string

UitextviewIos7

Uitextview Problem Overview


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!

Uitextview Solutions


Solution 1 - Uitextview

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

Solution 2 - Uitextview

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.

Solution 3 - Uitextview

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

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

Solution 4 - Uitextview

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.

Solution 5 - Uitextview

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

Solution 6 - Uitextview

textView.contentInset = UIEdgeInsetsMake(0.0, 0.0, 10.0, 0.0);

This will also address your issue

Solution 7 - Uitextview

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

Solution 8 - Uitextview

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

Solution 9 - Uitextview

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

textView.scrollEnabled = false

Try removing this and see what happens...

Solution 10 - Uitextview

   textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

This resolved the issue for me

Solution 11 - Uitextview

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionryanView Question on Stackoverflow
Solution 1 - UitextviewdavidisdkView Answer on Stackoverflow
Solution 2 - UitextviewgmyersView Answer on Stackoverflow
Solution 3 - UitextviewchrisView Answer on Stackoverflow
Solution 4 - Uitextviewcnotethegr8View Answer on Stackoverflow
Solution 5 - UitextviewWirsingView Answer on Stackoverflow
Solution 6 - UitextviewDimitrisView Answer on Stackoverflow
Solution 7 - UitextviewFabriceView Answer on Stackoverflow
Solution 8 - UitextviewaumansoftwareView Answer on Stackoverflow
Solution 9 - UitextviewnoobularView Answer on Stackoverflow
Solution 10 - UitextviewShilpiView Answer on Stackoverflow
Solution 11 - UitextviewAlbert RenshawView Answer on Stackoverflow