7

I've been using the following code in my view controller to update the content offset of a UITextView when the keyboard gets displayed:

- (void)keyboardWasShown:(NSNotification *)notification
{
    NSDictionary *info = [notification userInfo];
    CGRect keyboardRect = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

    UIEdgeInsets contentInsets = UIEdgeInsetsMake( 0.0, 0.0, keyboardRect.size.height, 0.0 );
    self.textView.contentInset = contentInsets;
    self.textView.scrollIndicatorInsets = contentInsets;
}

With the keyboard showing, manually scrolling the content of the UITextView to the bottom has it properly ending just above the top of the keyboard. -[UITexView scrollRangeToVisible:], however, doesn't seem to take into account the presence of the keyboard any more.

  • In iOS 6, the text view scrolled until the specified range was displayed just above the keyboard.
  • In iOS 7, the visibility appears to now be based on the frame of the text view and not the content inset, as it used to. So the view will only scroll when the range extends below the frame, and then it will scroll only enough to get that range visible at the bottom of the

Visually, here's what's happening. I built an inline search for my text view with controls to jump between the results (similar to searching in Safari). So in the text view shown here with search results as the user tapped the "next" button, the cyan selection would cycle down through the results. When the user went to the seventh result, the view would scroll until it was visible.

With the keyboard (from the UISearchBar) up on the same search results when the user went to the fifth search result, it would scroll to be just above the keyboard. But only in iOS 6. In iOS 7 no scrolling happens until going to the seventh search result like in the non-keyboard situation, and even then it scrolls the same amount so it's just visible below the bottom of the text view's frame.

Is this a known change in iOS 7? I'm using auto-layout so the next thing I'm going to try is to adjust the text view's bottom spacing constraint to shrink the entire view to avoid the problem, but want to check if there's way to still use my existing code under iOS 7.

Jeff Nouwen
  • 335
  • 1
  • 4
  • 9
  • Related here: http://stackoverflow.com/questions/18968735/how-to-re-size-uitextview-when-keyboard-shown-with-ios-7 – Ri_ Oct 03 '13 at 19:33

2 Answers2

5

Even though this has been already answered, I had the same problem while building my own UITextView subclass with search highlighting (it's available on my GitHub, if you're interested) and came up with a custom implementation of the scrollRangeToVisible: method. All you need to do is to adjust the contentInset and scrollIndicatorInset properties of your UITextView as you're already doing (related answer for casual Googlers reading this), then call:

[textView scrollRangeToVisible:range consideringInsets:YES];

I wrapped up the relevant code in a category, which also has a couple other useful methods to account for insets in iOS 7:

Note: you need them all due to how I organized this code in my subclass. Feel free to reorganize it to your liking.

@interface UITextView (insets)

// Scrolls to visible range, eventually considering insets
- (void)scrollRangeToVisible:(NSRange)range consideringInsets:(BOOL)considerInsets;

// Scrolls to visible rect, eventually considering insets
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated consideringInsets:(BOOL)considerInsets;

// Returns visible rect, eventually considering insets
- (CGRect)visibleRectConsideringInsets:(BOOL)considerInsets;

@end

@implementation UITextView (insets)

// Scrolls to visible range, eventually considering insets
- (void)scrollRangeToVisible:(NSRange)range consideringInsets:(BOOL)considerInsets
{
    if (considerInsets && (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1))
    {
        // Calculates rect for range
        UITextPosition *startPosition = [self positionFromPosition:self.beginningOfDocument offset:range.location];
        UITextPosition *endPosition = [self positionFromPosition:startPosition offset:range.length];
        UITextRange *textRange = [self textRangeFromPosition:startPosition toPosition:endPosition];
        CGRect rect = [self firstRectForRange:textRange];

        // Scrolls to visible rect
        [self scrollRectToVisible:rect animated:YES consideringInsets:YES];
    }
    else
        [self scrollRangeToVisible:range];
}

// Scrolls to visible rect, eventually considering insets
- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated consideringInsets:(BOOL)considerInsets
{
    if (considerInsets && (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1))
    {
        // Gets bounds and calculates visible rect
        CGRect bounds = self.bounds;
        UIEdgeInsets contentInset = self.contentInset;
        CGRect visibleRect = [self visibleRectConsideringInsets:YES];

        // Do not scroll if rect is on screen
        if (!CGRectContainsRect(visibleRect, rect))
        {
            CGPoint contentOffset = self.contentOffset;
            // Calculates new contentOffset
            if (rect.origin.y < visibleRect.origin.y)
                // rect precedes bounds, scroll up
                contentOffset.y = rect.origin.y - contentInset.top;
            else
                // rect follows bounds, scroll down
                contentOffset.y = rect.origin.y + contentInset.bottom + rect.size.height - bounds.size.height;
            [self setContentOffset:contentOffset animated:animated];
        }
    }
    else
        [self scrollRectToVisible:rect animated:animated];
}

// Returns visible rect, eventually considering insets
- (CGRect)visibleRectConsideringInsets:(BOOL)considerInsets
{
    CGRect bounds = self.bounds;
    if (considerInsets)
    {
        UIEdgeInsets contentInset = self.contentInset;
        CGRect visibleRect = self.bounds;
        visibleRect.origin.x += contentInset.left;
        visibleRect.origin.y += contentInset.top;
        visibleRect.size.width -= (contentInset.left + contentInset.right);
        visibleRect.size.height -= (contentInset.top + contentInset.bottom);
        return visibleRect;
    }
    return bounds;
}

@end
Community
  • 1
  • 1
Ivano.Bilenchi
  • 501
  • 4
  • 14
  • Sorry it took a while to respond to this, but your code worked great (though I did have to change line 31 from `super` to `self` due to UITextView's inheritance). I've marked your solution as the accepted answer since it's the "lightest" workaround. – Jeff Nouwen Nov 12 '13 at 04:06
  • @Exile.90 With your code there's still a bug in iOS 7.0.4. When you scroll text view with some text then tap it and hit return (without typing anything) text view occasionally jumps to another location. – Indoor Dec 10 '13 at 18:19
  • I am aware of that. Unfortunately, that's an UITextView bug I'm still trying to find a workaround for. In my UITextView subclass, you can just type another character and it will jump again at the cursor location (this is a result of my fix to the "caret out of visible range" bug). UITextView is really broken beyond repair, sigh. If you're interested, check out [ICTextView](https://github.com/Exile90/ICTextView). It has quite a few bugfixes over the standard UITextView, you may find it much more bearable to work with. Also, feel free to contribute to it, if you manage to find a decent fix. – Ivano.Bilenchi Dec 10 '13 at 21:58
  • Tried your code, but works exactly like the original scrollRangeToVisible ignoring the keyboard inputAccessoryView bounds. Are you considering the the existence of an inputAccessoryView? – Pedro Borges Apr 21 '15 at 20:55
  • 1
    @PedroBorges It is not the responsibility of the code I wrote to take `inputAccessoryView` into account. You should set the `contentInset` property of `UITextView` according to the actual size of your keyboard, taking `inputAccessoryView` into account. Once you do that, the code will work as expected. You can check out the sample app I wrote for [ICTextView](https://github.com/Exile90/ICTextView), which uses an `UIToolBar` as `inputAccessoryView`. – Ivano.Bilenchi Apr 22 '15 at 17:49
  • Thanks for the answer, I was unaware of the UITextView.contentInset property. – Pedro Borges Apr 22 '15 at 19:26
3

This seems to be a bug in iOS7. I am using following code as a work around (heavily inspired by the answers to following questions: How to re-size UITextView when keyboard shown with iOS 7).

CGRect caret_rect = [_editTextView caretRectForPosition:_editTextView.selectedTextRange.end];
UIEdgeInsets insets = _editTextView.contentInset;
CGRect visible_rect = _editTextView.bounds;
visible_rect.size.height -= (insets.top + insets.bottom);
visible_rect.origin.y = _editTextView.contentOffset.y;
if(!CGRectContainsRect(visible_rect, caret_rect)) {
    CGFloat new_offset = MAX((caret_rect.origin.y + caret_rect.size.height) - visible_rect.size.height - _editTextView.contentInset.top,  - _editTextView.contentInset.top);
    [_editTextView setContentOffset:CGPointMake(0, new_offset) animated:NO];
}

Oddly, it is not possible to change animated to YES in the last call.

I will file a bug report with Apple.

Community
  • 1
  • 1
Klaus Thul
  • 675
  • 5
  • 7
  • Indeed, I surmised that I'd have to either change the frame of the text view or do something similar to your code snippet. The more I thought about it though, I'm not entirely sure it's a bug per se, or at least Apple might not think of it as one. Specifically, it's due to the keyboard being translucent now in iOS 7, and just like a translucent nav bar it's not being considered as obscuring the content below it. Ah well, I'll file a bug on it as well! – Jeff Nouwen Oct 15 '13 at 17:56