UITextView cursor under the frame when changing the frame

I have a UIViewCOntroller that contains a UITextView . When the keyboard appears, I change it like this:

 #pragma mark - Responding to keyboard events - (void)keyboardDidShow:(NSNotification *)notification { NSDictionary* info = [notification userInfo]; CGRect keyboardSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect newTextViewFrame = self.textView.frame; newTextViewFrame.size.height -= keyboardSize.size.height + 70; self.textView.frame = newTextViewFrame; self.textView.backgroundColor = [UIColor yellowColor]; } - (void)keyboardWillHide:(NSNotification *)notification { NSDictionary* info = [notification userInfo]; CGRect keyboardSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect newTextViewFrame = self.textView.frame; newTextViewFrame.size.height += keyboardSize.size.height - 70; self.textView.frame = newTextViewFrame; } 

The TextView seems to have the correct meaning, but when the user enters the cursor, it ends โ€œoutsideโ€ the textView frame. See the picture below:

enter image description here

The yellow area is the UITextView frame (I donโ€™t know which blue line is next to the R key). I find it wired enough. I use iOS7 if that matters.

Any ideas or tips?

Update

I have a subclass of UITextView that draws horizontal lines with the following method (if that matters):

 - (void)drawRect:(CGRect)rect { //Get the current drawing context CGContextRef context = UIGraphicsGetCurrentContext(); //Set the line color and width CGContextSetStrokeColorWithColor(context, [UIColor colorWithRed:229.0/255.0 green:244.0/255.0 blue:255.0/255.0 alpha:1].CGColor); CGContextSetLineWidth(context, 1.0f); //Start a new Path CGContextBeginPath(context); //Find the number of lines in our textView + add a bit more height to draw lines in the empty part of the view NSUInteger numberOfLines = (self.contentSize.height + rect.size.height) / self.font.lineHeight; CGFloat baselineOffset = 6.0f; //iterate over numberOfLines and draw each line for (int x = 0; x < numberOfLines; x++) { //0.5f offset lines up line with pixel boundary CGContextMoveToPoint(context, rect.origin.x, self.font.lineHeight*x + 0.5f + baselineOffset); CGContextAddLineToPoint(context, rect.size.width, self.font.lineHeight*x + 0.5f + baselineOffset); } // Close our Path and Stroke (draw) it CGContextClosePath(context); CGContextStrokePath(context); } 
+16
ios objective-c cocoa-touch uikeyboard uitextview
Aug 25 '13 at 17:26
source share
9 answers

Instead of resizing the border, why not give your text the look of a contentInset (and the corresponding scrollIndicatorInsets )? Remember that text views are actually scrollviews. This is the right way to handle keyboard (or other) interference.

For more information on contentInset see this question.




This seems to be insufficient. Still use inserts as this is more correct (especially on iOS7, where the keyboard is transparent), but you will also need additional processing for the caret:

 - (void)viewDidLoad { [super viewDidLoad]; [self.textView setDelegate:self]; self.textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil]; } - (void)_keyboardWillShowNotification:(NSNotification*)notification { UIEdgeInsets insets = self.textView.contentInset; insets.bottom += [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; self.textView.contentInset = insets; insets = self.textView.scrollIndicatorInsets; insets.bottom += [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; self.textView.scrollIndicatorInsets = insets; } - (void)_keyboardWillHideNotification:(NSNotification*)notification { UIEdgeInsets insets = self.textView.contentInset; insets.bottom -= [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height; self.textView.contentInset = insets; insets = self.textView.scrollIndicatorInsets; insets.bottom -= [notification.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height; self.textView.scrollIndicatorInsets = insets; } - (void)textViewDidBeginEditing:(UITextView *)textView { _oldRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end]; _caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(_scrollCaretToVisible) userInfo:nil repeats:YES]; } - (void)textViewDidEndEditing:(UITextView *)textView { [_caretVisibilityTimer invalidate]; _caretVisibilityTimer = nil; } - (void)_scrollCaretToVisible { //This is where the cursor is at. CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end]; if(CGRectEqualToRect(caretRect, _oldRect)) return; _oldRect = caretRect; //This is the visible rect of the textview. CGRect visibleRect = self.textView.bounds; visibleRect.size.height -= (self.textView.contentInset.top + self.textView.contentInset.bottom); visibleRect.origin.y = self.textView.contentOffset.y; //We will scroll only if the caret falls outside of the visible rect. if(!CGRectContainsRect(visibleRect, caretRect)) { CGPoint newOffset = self.textView.contentOffset; newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height + 5, 0); [self.textView setContentOffset:newOffset animated:YES]; } } -(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } 

A lot of work, Apple should provide a better way to handle carriage, but it works.

+19
Sep 03 '13 at 6:43
source share

All the other answers that I tried behaved a little strange for me. Using NSTimer to perform scrolling also meant that the user could not scroll up, as the carriage would then end off-screen and it would immediately scroll back. In the end, I took the initial approach to changing the UITextView frame on keyboard notification events, and then added the following methods:

 - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { // Whenever the user enters text, see if we need to scroll to keep the caret on screen [self scrollCaretToVisible]; return YES; } - (void)scrollCaretToVisible { //This is where the cursor is at. CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end]; // Convert into the correct coordinate system caretRect = [self.view convertRect:caretRect fromView:self.textView]; if(CGRectEqualToRect(caretRect, _oldRect)) { // No change return; } _oldRect = caretRect; //This is the visible rect of the textview. CGRect visibleRect = self.textView.frame; //We will scroll only if the caret falls outside of the visible rect. if (!CGRectContainsRect(visibleRect, caretRect)) { // Work out how much the scroll position would have to change by to make the cursor visible CGFloat diff = (caretRect.origin.y + caretRect.size.height) - (visibleRect.origin.y + visibleRect.size.height); // If diff < 0 then this isn't to do with the iOS7 bug, so ignore if (diff > 0) { // Scroll just enough to bring the cursor back into view CGPoint newOffset = self.textView.contentOffset; newOffset.y += diff; [self.textView setContentOffset:newOffset animated:YES]; } } } 

Works like a charm for me

+6
Oct 05 '13 at 16:30
source share

Many answers already, I found that in my case it is actually much simpler. On the contentInset keyboard, I adjust the text representation of the contentInset and save the full frame. And although scrollRangeToVisible: doesn't work for me, like for many others, scroll view methods (from which the UITextView inherits) work fine. This works for me:

 - (void)textViewDidChange:(UITextView *)textView { CGRect caret = [_textView caretRectForPosition:_textView.selectedTextRange.end]; [_textView scrollRectToVisible:caret animated:YES]; } 
+4
Apr 15 '14 at 6:11
source share

Anders and Leo Nathan have great solutions. However, I need to slightly modify their answers to make the scrollbar work correctly with the contentInset. The problem I ran into was that textViewDidBeginEditing: is called before keyboardWasShown: so changing the contentInset is not reflected the first time. Here is what I did:

In .h

 @interface NoteDayViewController : UIViewController <UITextViewDelegate> { UIEdgeInsets noteTextViewInsets; UIEdgeInsets noteTextViewScrollIndicatorInsets; CGRect oldRect; NSTimer *caretVisibilityTimer; float noteViewBottomInset; } @property (weak, nonatomic) IBOutlet UITextView *noteTextView; 

In .m

 - (void)registerForKeyboardNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardDidShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillBeHidden:) name:UIKeyboardWillHideNotification object:nil]; } - (void)keyboardWasShown:(NSNotification*)aNotification { CGFloat kbHeight = // get the keyboard height following your usual method UIEdgeInsets contentInsets = noteTextViewInsets; contentInsets.bottom = kbHeight; noteTextView.contentInset = contentInsets; UIEdgeInsets scrollInsets = noteTextViewScrollIndicatorInsets; scrollInsets.bottom = kbHeight; noteTextView.scrollIndicatorInsets = scrollInsets; [noteTextView setNeedsDisplay]; } - (void)keyboardWillBeHidden:(NSNotification*)aNotification { noteTextView.contentInset = noteTextViewInsets; noteTextView.scrollIndicatorInsets = noteTextViewScrollIndicatorInsets; [noteTextView setNeedsDisplay]; } - (void)textViewDidBeginEditing:(UITextView *)textView { oldRect = [noteTextView caretRectForPosition:noteTextView.selectedTextRange.end]; noteViewBottomInset = noteTextView.contentInset.bottom; caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(scrollCaretToVisible) userInfo:nil repeats:YES]; } - (void)textViewDidEndEditing:(UITextView *)textView { [caretVisibilityTimer invalidate]; caretVisibilityTimer = nil; } - (void)scrollCaretToVisible { // This is where the cursor is at. CGRect caretRect = [noteTextView caretRectForPosition:noteTextView.selectedTextRange.end]; // test if the caret has moved OR the bottom inset has changed if(CGRectEqualToRect(caretRect, oldRect) && noteViewBottomInset == noteTextView.contentInset.bottom) return; // reset these for next time this method is called oldRect = caretRect; noteViewBottomInset = noteTextView.contentInset.bottom; // this is the visible rect of the textview. CGRect visibleRect = noteTextView.bounds; visibleRect.size.height -= (noteTextView.contentInset.top + noteTextView.contentInset.bottom); visibleRect.origin.y = noteTextView.contentOffset.y; // We will scroll only if the caret falls outside of the visible rect. if (!CGRectContainsRect(visibleRect, caretRect)) { CGPoint newOffset = noteTextView.contentOffset; newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height, 0); [noteTextView setContentOffset:newOffset animated:NO]; // must be non-animated to work, not sure why } } 
+3
Oct 01 '13 at 17:04 on
source share

This is what I finished and something similar to work:

 - (void)textViewKeyboardWillShow:(NSNotification *)notification { NSDictionary* info = [notification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; // self.textViewBottomSpace.constant = NSLayoutConstraint in IB (bottom position) self.textViewBottomSpace.constant = kbSize.height + 70; [self.textView setNeedsDisplay]; } - (void)textViewKeyboardWillHide:(NSNotification *)notification { self.textViewBottomSpace.constant = 0; [self.textView setNeedsDisplay]; } - (void)scrollCaretToVisible { //This is where the cursor is at. CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end]; if(CGRectEqualToRect(caretRect, _oldRect)) return; _oldRect = caretRect; //This is the visible rect of the textview. CGRect visibleRect = self.textView.bounds; visibleRect.size.height -= (self.textView.contentInset.top + self.textView.contentInset.bottom); visibleRect.origin.y = self.textView.contentOffset.y; //We will scroll only if the caret falls outside of the visible rect. if(!CGRectContainsRect(visibleRect, caretRect)) { CGPoint newOffset = self.textView.contentOffset; newOffset.y = MAX((caretRect.origin.y + caretRect.size.height) - visibleRect.size.height + 10, 0); [self.textView setContentOffset:newOffset animated:YES]; } } - (void)textViewDidEndEditing:(UITextView *)textView { [_caretVisibilityTimer invalidate]; _caretVisibilityTimer = nil; } - (void)textViewDidBeginEditing:(UITextView *)textView { self.oldRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.end]; self.caretVisibilityTimer = [NSTimer scheduledTimerWithTimeInterval:0.3 target:self selector:@selector(scrollCaretToVisible) userInfo:nil repeats:YES]; } 
+1
Sep 20 '13 at 14:59
source share

A simpler solution to this problem is to update the text presentation frame in response to the delegate method textViewDidBegingEditing. For more information, see the following:

How to resize UITextView when displaying keyboard using iOS 7

+1
Sep 27 '13 at 13:11
source share

For those who have a UITextView inside a UIScrollView , where iOS <7 made sure to scroll the carriage: "How it works with iOS 7 (as well as 5 and 6).

 // This is the scroll view reference @property (weak, nonatomic) IBOutlet UIScrollView *scrollView; // Track the current UITextView @property (weak, nonatomic) UITextView *activeField; - (void)textViewDidBeginEditing:(UITextView *)textView { self.activeField = textView; } - (void)textViewdDidEndEditing:(UITextView *)textView { self.activeField = nil; } // Setup the keyboard observers that take care of the insets & initial scrolling [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardDidShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillBeHidden:) name:UIKeyboardWillHideNotification object:nil]; - (void)keyboardWasShown:(NSNotification*)aNotification { // Set the insets above the keyboard NSDictionary* info = [aNotification userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; UIEdgeInsets insets = self.vForm.contentInset; insets.bottom += kbSize.height; self.vForm.contentInset = insets; insets = self.vForm.scrollIndicatorInsets; insets.bottom += kbSize.height; self.vForm.scrollIndicatorInsets = insets; // Scroll the active text field into view CGRect aRect = self.vForm.frame; aRect.size.height -= kbSize.height; CGPoint scrollPoint = CGPointMake(0.0, self.activeField.frame.origin.y); [self.scrollView setContentOffset:scrollPoint animated:YES]; } - (void)keyboardWillBeHidden:(NSNotification*)aNotification { UIEdgeInsets contentInsets = UIEdgeInsetsZero; self.vForm.contentInset = contentInsets; self.vForm.scrollIndicatorInsets = contentInsets; } // This is where the magic happens. Set the class with this method as the UITextView delegate. - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { // Scroll the textview to the caret position [textView scrollRangeToVisible:textView.selectedRange]; // Scroll the scrollview to the caret position within the textview CGRect targetRect = [textView caretRectForPosition:textView.selectedTextRange.end]; targetRect.origin.y += self.activeField.frame.origin.y; [self.scrollView scrollRectToVisible:targetRect animated:YES]; return YES; } 

I tried to include most of the required glue code. The only thing missing is to set the UITextView delegate and reject the keyboard.

Took 2-3 days to find out what worked before. Thanks, Apple.

+1
Oct 25 '13 at 19:51
source share

The remark of Angel Naydenov above is true, especially in cases such as switching from English to Japanese, which shows tips.

When switching keyboards, UIKeyboardWillShowNotification is UIKeyboardWillShowNotification , but UIKeyboardWillHideNotification not called.

So, you have to adjust the insert to use the absolute value, and not use += .

Incomparably, [self.textView setContentOffset:newOffset animated:YES]; It doesnโ€™t actually change the graphics in iOS 7.1 after the keyboard is shown a second time, which is probably an error. The workaround that I used replaces

 [self.textView setContentOffset:newOffset animated:YES]; 

from

 [UIView animateWithDuration:.25 animations:^{ self.textView.contentOffset = newOffset; }]; 
+1
Feb 16 '15 at 2:57
source share

Leo Nathan, you started well, but your work was relatively ineffective. Here is the best way to do this with less code:

 // Add Keyboard Notification Listeners in ViewDidLoad [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil]; // And Add The Following Methods - (void)_keyboardWillShowNotification:(NSNotification*)notification { CGRect textViewFrame = self.textView.frame; textViewFrame.size.height -= ([notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height + 4.0); self.textView.frame = textViewFrame; } - (void)_keyboardWillHideNotification:(NSNotification*)notification { CGRect textViewFrame = self.textView.frame; textViewFrame.size.height += ([notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height + 4.0); self.textView.frame = textViewFrame; } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { NSRange typingRange = NSMakeRange(textView.text.length - 1, 1); [textView scrollRangeToVisible:typingRange]; return YES; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } 
0
Oct 12 '13 at
source share



All Articles