7
votes

I've searched high and low and can't seem to find an answer. The closest thing I've seen is here: UITextView cursor below frame when changing frame

Sorry, no screenshots as I have (nearly) no reputation, but it looks similar to the linked SO post.

When I run the app on iOS6, things work perfectly (content scrolls with the cursor to keep it on screen), but on iOS7, the cursor goes one line beyond the end of the UITextView. I tried adding UIEdgeInsets to move the content, but when the user is actively entering text, it just keeps adding a new line until the cursor is below the end of the text view.

My layout consists of a Label (headerText) with a UITextView (textView) below it. This view is shown from a tab bar. There is a keyboard input accessory that is added, but it's height is calculated into the keyboard height automatically before the function is called.

Here is the function I use to resize my views, called from keyboard show/hide delegate, rotate, initial layout, etc:

-(void)resizeViewsWithKbdHeight:(float)kbHeight
{
    //set the header label frame

    //set constraints
    //total view width - 30 (15 each for left and right padding)
    CGFloat labelWidth = ([[[self navigationController] view] bounds].size.width - 30);
    CGSize constraintSize = {labelWidth, CGFLOAT_MAX};
    //calculate needed height for header label
    CGSize textSize = [[self headerText] sizeWithFont:[UIFont systemFontOfSize:17.0f]
                                    constrainedToSize:constraintSize
                                        lineBreakMode:NSLineBreakByWordWrapping];
    //build and set frame for header label:
    //make it the same as the old
    CGRect headerTempSize = [[self headerLabel] frame];
    //except for the height
    headerTempSize.size.height = textSize.height;
    //and set it
    [[self headerLabel] setFrame:headerTempSize];

    //correct the placement of the UITextView, so it's under the header label

    //build a new frame based on current textview frame
    CGRect newFrame = [[self textView] frame];

    //get the y position of the uitextview, the +8 is the padding between header and uitextview
    CGFloat vertPadding = [[self headerLabel] frame].origin.y + [[self headerLabel] frame].size.height + 8;

    //bump it down vertically
    newFrame.origin.y = vertPadding;

    //bump things down by the amount of the navigation bar and status bar
    float offscreenBump = [[[self navigationController] navigationBar] frame].origin.y + [[[self navigationController] navigationBar] frame].size.height;

    //if we aren't showing the keyboard, add the height of the tab bar
    if(kbHeight == 0) {
        offscreenBump += [[[self tabBarController] tabBar] frame].size.height;
    }

    //calculate the new height of the textview, the +9 is for padding below the text view
    CGFloat newHeight = [[[self navigationController] view] bounds].size.height - ([[self textView] frame].origin.y + 9 + kbHeight + offscreenBump);

    //resize the height as calculated
    newFrame.size.height = newHeight;

    //set textview frame to this new frame
    [[self textView] setFrame:newFrame];
}

I'm trying to support for iOS5, so no AutoLayout.

It's possible I'm being incredibly naive about how I'm doing things.

Thanks in advance!

2
Ya it's a pain to resize views when keyboard invokes.. But there is an open source I found in github named EKKeyboardAvoiding. If you want to try, that would be fine. It will resize the scrollview as you need when keyboard invokes. You can use that for any scrollview subclasses.Dinesh Raja

2 Answers

7
votes

This appears to be a bug in iOS 7. The only way I've found to correct this issue is to add a delegate for the UITextView and implement textViewDidChangeSelection, resetting the view to show the selection like this:

#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v)  ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)

- (void) textViewDidChangeSelection: (UITextView *) tView {
    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")) {
        [tView scrollRangeToVisible:[tView selectedRange]];
    }
}
6
votes

I found a hackier yet more effective way to deal with the problem:

- (void)textViewDidChangeSelection:(UITextView *)textView
{
    if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
        if ([textView.text characterAtIndex:textView.text.length-1] != ' ') {
            textView.text = [textView.text stringByAppendingString:@" "];
        }

        NSRange range0 = textView.selectedRange;
        NSRange range = range0;
        if (range0.location == textView.text.length) {
            range = NSMakeRange(range0.location - 1, range0.length);
        } else if (range0.length > 0 &&
                   range0.location + range0.length == textView.text.length) {
            range = NSMakeRange(range0.location, range0.length - 1);
        }
        if (!NSEqualRanges(range, range0)) {
            textView.selectedRange = range;
        }
    }
}

Basically, I make sure that there's always a trailing space in the text field. Then, if the user tries to change the selection such that the space is revealed, change the selection. By always keeping a space ahead of the cursor, the field scrolls itself as it's supposed to.

Finally, if you need to, remove the trailing space when copying the text into your model (not shown.)