UIScrollView Content Insets not working for Keyboard Height - ios

I am trying to move a UIScrollView when the keyboard hides a UITextField by changing the size using the contentInsets as it is shown.
However, it's not working for the keyboard height. The keyboard height comes as 216, but it only stops scrolling at the correct location if I set the bottom inset to 515 for iPhone portrait mode and 310 for iPhone landscape mode. Why would these dimensions be so different? I don't want to hardcode these arbitrary values in.
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.frame = self.parentViewController.view.frame;
[self.textView becomeFirstResponder];
NSLog(#"scrollview: %f,%f, parent: %f,%f", self.view.frame.size.height, self.view.frame.size.width, self.parentViewController.view.frame.size.height, self.parentViewController.view.frame.size.width);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)keyboardWasShown:(NSNotification *)notification
{
if([[UIDevice currentDevice]userInterfaceIdiom]==UIUserInterfaceIdiomPhone) {
// Step 1: Get the size of the keyboard.
CGFloat keyboardHeight = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
// Step 2: Adjust the bottom content inset of your scroll view by the keyboard height.
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardHeight, 0.0);
((UIScrollView*)self.view).contentInset = contentInsets;
((UIScrollView*)self.view).scrollIndicatorInsets = contentInsets;
// Step 3: Scroll the target text field into view.
CGRect aRect = self.view.frame;
aRect.size.height = aRect.size.height - keyboardHeight;
if (!CGRectContainsPoint(aRect, self.textView.frame.origin) ) {
CGPoint scrollPoint = CGPointMake(0.0, self.textView.frame.origin.y - keyboardHeight);
[((UIScrollView*)self.view) setContentOffset:scrollPoint animated:YES];
}
}
}
- (void) keyboardWillHide:(NSNotification *)notification {
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
((UIScrollView*)self.view).contentInset = contentInsets;
((UIScrollView*)self.view).scrollIndicatorInsets = contentInsets;
}
Edit:
Before the keyboard is open, i print this out:
NSLog(#"scrollview: %f,%f, parent: %f,%f", self.view.frame.size.height, self.view.frame.size.width, self.parentViewController.view.frame.size.height, self.parentViewController.view.frame.size.width);
and it prints this:
scrollview: 431.000000,320.000000, parent: 431.000000,320.000000

I think that the main issue is that you need to use UIKeyboardFrameEndUserInfoKey instead of UIKeyboardFrameBeginUserInfoKey.
That being said, I have written a different version of this method that will work even if the scrollview is not placed at the bottom of the screen and may work better for you:
Note that I use self.activeTextField instead of self.textField.
// Called when the UIKeyboardDidShowNotification is sent.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
// Calculate the frame of the scrollview, in self.view's coordinate system
UIScrollView *scrollView = self.scrollView;
CGRect scrollViewRect = [self.view convertRect:scrollView.frame fromView:scrollView.superview];
// Calculate the frame of the keyboard, in self.view's coordinate system
NSDictionary* info = [aNotification userInfo];
CGRect kbRect = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
kbRect = [self.view convertRect:kbRect fromView:nil];
// Figure out where the two frames overlap, and set the content offset of the scrollview appropriately
CGRect hiddenScrollViewRect = CGRectIntersection(scrollViewRect, kbRect);
if (!CGRectIsNull(hiddenScrollViewRect))
{
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, hiddenScrollViewRect.size.height, 0.0);
scrollView.contentInset = contentInsets;
scrollView.scrollIndicatorInsets = contentInsets;
}
[self scrollToActiveTextField];
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
self.scrollView.contentInset = UIEdgeInsetsZero;
self.scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;
}
- (void)scrollToActiveTextField
{
if (self.activeTextField)
{
CGRect visibleRect = self.activeTextField.frame;
visibleRect = [self.scrollView convertRect:visibleRect fromView:self.activeTextField.superview];
visibleRect = CGRectInset(visibleRect, 0.0f, -5.0f);
[self.scrollView scrollRectToVisible:visibleRect animated:YES];
}
}

set delegate of your scrollView as self whereever you are crating scrollview in your code
((UIScrollView*)self.view).delegate=self;
then
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.frame = self.parentViewController.view.frame;
[self.textView becomeFirstResponder];
NSLog(#"scrollview: %f,%f, parent: %f,%f", self.view.frame.size.height, self.view.frame.size.width, self.parentViewController.view.frame.size.height, self.parentViewController.view.frame.size.width);
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)keyboardWasShown:(NSNotification *)notification
{
if([[UIDevice currentDevice]userInterfaceIdiom]==UIUserInterfaceIdiomPhone) {
// Step 1: Get the size of the keyboard.
CGFloat keyboardHeight = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size.height;
// Step 2: Adjust the bottom content inset of your scroll view by the keyboard height.
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardHeight, 0.0);
((UIScrollView*)self.view).contentInset = contentInsets;
((UIScrollView*)self.view).scrollIndicatorInsets = contentInsets;
((UIScrollView*)self.view).scrollEnabled=YES;
}
}
- (void) keyboardWillHide:(NSNotification *)notification {
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
((UIScrollView*)self.view).contentInset = contentInsets;
((UIScrollView*)self.view).scrollIndicatorInsets = contentInsets;
[((UIScrollView*)self.view) setContentOffset:CGPointMake(0.0f, 0.0f) animated:TRUE];
((UIScrollView*)self.view).contentSize = CGSizeMake(((UIScrollView*)self.view).contentSize.width, ((UIScrollView*)self.view).contentSize.height);
((UIScrollView*)self.view).scrollEnabled=NO;
}
#pragma mark - set scrollView content position
-(void)scrollViewToCenterOfScreen:(UIView *)theView
{
CGFloat theViewY = theView.center.y;
CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame];
CGFloat avaliableHeight = applicationFrame.size.height - 300;
CGFloat y = theViewY - avaliableHeight / 2.0;
if(y<0)
y = 0;
[((UIScrollView*)self.view) setContentOffset:CGPointMake(0,y) animated:YES];
}
#pragma -mark UITextField Delegate methods
- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
{
[self scrollViewToCenterOfScreen:textField];
return YES;
}
Now set delegate of your text field. Whenever textFieldShouldBeginEditing: your scrollview automatically move on that. scrollViewToCenterOfScreen: method set your scrollview position at textfiled position.
It definitely set your scrollview content insets as well as setContentOffset. Please let me know if you still facing this problem. Thanks

Related

Move visibility of UIScrollView area automatically

I want to make my view automatically moving up if keyboard appears. Already use apple's code here and it works well.
This is how I manage my object, so I create a UIScrollView that covers UIView. This UIView consists of UITextField and UIButton.
This is how I adjust my view when keyboard appears.
#pragma mark - Keyboard Handling
// Call this method somewhere in your view controller setup code.
- (void)registerForKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification object:nil];
}
// Called when the UIKeyboardDidShowNotification is sent.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0);
_scrollView.contentInset = contentInsets;
_scrollView.scrollIndicatorInsets = contentInsets;
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
CGRect aRect = self.view.frame;
aRect.size.height -= kbSize.height;
if (!CGRectContainsPoint(aRect, _mainView.frame.origin) ) {
[self.scrollView scrollRectToVisible:_mainView.frame animated:YES];
}
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
_scrollView.contentInset = contentInsets;
_scrollView.scrollIndicatorInsets = contentInsets;
}
But I think there is a point that make that weird. When keyboard showed up, it scrolls and my UITextField become visible. But I thought it was too tight.
In my opinion, it would be better if my UITextField moving up a little bit. My question is, how can I set its scroll visibility? It looks like some variable should be added with some constant here
CGRect aRect = self.view.frame;
aRect.size.height -= kbSize.height;
if (!CGRectContainsPoint(aRect, _mainView.frame.origin) ) {
[self.scrollView scrollRectToVisible:_mainView.frame animated:YES];
}
Note:
Result that I wanted
Thank you so much, a little hint would be appreciated.
The simplest solution would be to move your view (or scrollview) up when keyboard opens.
- (void)keyboardWillShow:(NSNotification*)notification{
[self.view setFrame:CGRectMake(0,-100, self.view.frame.size.width, self.view.frame.size.height)]; // where 100 is the offset
[self.view setNeedsDisplay];
}
- (void)keyBoardWillHide:(NSNotification*)notification{
[self.view setFrame:CGRectMake(0,0, self.view.frame.size.width, self.view.frame.size.height)];
[self.view setNeedsDisplay];
}
Solved
I solved this by manage its content inset by adding some number.
In keyboardWasShown:, I added its content inset by height of my textfield and button. Let say it was 100 total, so this is it.
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height+100, 0.0);
Thank you so much.

Moving UIView on top of Keyboard with AutoLayout

I have a chat view with UITableView on all of the screen and on the bottom of the screen there is a UIView that holds 2 UIButtons and UITextField.
What I want to do is when the user clicks on the UITextField and the keyboard shows, the view will change his location to the top of the keyboard. For some reason I think that the AutoLayout is preventing it from moving, how could I know?
This is what I did so far:
- (void)registerForKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification object:nil];
}
// Called when the UIKeyboardDidShowNotification is sent.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets = UIEdgeInsetsMake(kbSize.height, 0.0, kbSize.height, 0.0);
self.tableChat.contentInset = contentInsets;
self.tableChat.scrollIndicatorInsets = contentInsets;
CGRect inputTextView = self.accessoryViewForChat.frame;
inputTextView.origin.y = inputTextView.origin.y - kbSize.height;
self.accessoryViewForChat.frame = inputTextView;
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
CGRect aRect = self.view.frame;
aRect.size.height -= kbSize.height;
if (!CGRectContainsPoint(aRect, self.activeTextField.frame.origin) ) {
[self.tableChat scrollRectToVisible:self.activeTextField.frame animated:NO];
}
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
self.tableChat.contentInset = contentInsets;
self.tableChat.scrollIndicatorInsets = contentInsets;
CGRect inputTextView = self.accessoryViewForChat.frame;
inputTextView.origin.y = inputTextView.origin.y + kbSize.height;
self.accessoryViewForChat.frame = inputTextView;
}
#pragma mark UITextFieldDelegate methods
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
self.activeTextField = textField;
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
self.activeTextField = nil;
}

iOS: Hidden parts of table view behind keyboard issue

I've been struggling with this for hours now and worked through a lot on stackoverflow and the docs. However I cannot get the apple code running properly in my app.
Here's the complete apple code and from there I afterwards I ask my questions:
// Call this method somewhere in your view controller setup code.
- (void)registerForKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWasShown:)
name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification object:nil];
}
// Called when the UIKeyboardDidShowNotification is sent.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0);
scrollView.contentInset = contentInsets;
scrollView.scrollIndicatorInsets = contentInsets;
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
CGRect aRect = self.view.frame;
aRect.size.height -= kbSize.height;
if (!CGRectContainsPoint(aRect, activeField.frame.origin) ) {
[self.scrollView scrollRectToVisible:activeField.frame animated:YES];
}
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
scrollView.contentInset = contentInsets;
scrollView.scrollIndicatorInsets = contentInsets;
}
1.: Why do they adjust the insets here? I still don't get that:
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0);
scrollView.contentInset = contentInsets;
scrollView.scrollIndicatorInsets = contentInsets;
2: I'm writing with this code a subclass of UITableView in order to use it in all my table views in my app. I never have just a plain table view, usually it's embedded in a superview. Or let's say I usually have a view at the top of my screen, one at the bottom and in the middle between I have my tableview. I want to move my table view up only if a textfield is hidden below the keyboard and only so much that this textfield is visible. I'm saying that because at least in one screen with the keyboard shown only about 2 cells at once are visible. And with the apple code the table view get's scrolled up waaaay to much. I've adjusted the code so far like this (my subclass of tableview has two properties, activeField and activeFieldOriginInSuperView (without that I think it's not possible?) But it's moving the table view still way too much up and the relevant textfield isn't visible anymore...any idea what's wrong?
- (void)keyboardWasShown:(NSNotification*)aNotification
{
NSDictionary* info = [aNotification userInfo];
CGSize keyBoardSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
CGRect tableFrame = self.frame;
tableFrame.size.height -= keyBoardSize.height;
CGPoint activeFieldOrigin = self.activeFieldOriginInSuperView;
activeFieldOrigin.y -= self.contentOffset.y;
CGPoint activeFieldLowerEnd = CGPointMake(activeFieldOrigin.x, activeFieldOrigin.y + self.activeField.frame.size.height + 5);
if (!CGRectContainsPoint(tableFrame, activeFieldLowerEnd) ) {
CGPoint scrollPoint = CGPointMake(0.0, activeFieldLowerEnd.y - tableFrame.size.height);
[self setContentOffset:scrollPoint animated:YES];
}
}
It will definitely work. It works for me for last 50 applications.
Try this
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardDidHide:) name:UIKeyboardWillHideNotification object:nil];
- (void)keyboardDidShow: (NSNotification *) notif{
[UIView animateWithDuration:0.5f delay:0.0f options: UIViewAnimationOptionCurveEaseInOut animations:^{
_scrollView.contentOffset = CGPointMake(0, txtId_.frame.origin.y - 200);
} completion:^(BOOL finished){ }];
}
- (void)keyboardDidHide: (NSNotification *) notif{
[UIView animateWithDuration:0.5f delay:0.0f options: UIViewAnimationOptionCurveEaseInOut animations:^{
_scrollView.contentOffset = self.view.frame.origin;
} completion:^(BOOL finished){ }];
}

iOS7 UIScrollView contentInset not working

When the keyboard was hidden, the scrollview should back to it's origin contentInset, but it's not working in iOS7. Setting scrollview's contentInset when keyboard was shown is working but when the keyboard was hidden, the scrollview's contentInset can't set to inset zero.
The code:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWasShown:) name:UIKeyboardDidShowNotification object:Nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWasHidden:) name:UIKeyboardDidHideNotification object:nil];
}
- (void)keyboardWasShown:(NSNotification *)notif
{
CGSize keyboardSize = [[[notif userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0);
UIScrollView *scrollView = (UIScrollView *)self.view;
scrollView.contentInset = contentInsets;
scrollView.scrollIndicatorInsets = contentInsets;
CGRect rect = self.view.frame;
rect.size.height -= keyboardSize.height;
if (!CGRectContainsPoint(rect, self.wishContentField.frame.origin)) {
CGPoint point = CGPointMake(0, self.wishContentField.frame.origin.y - keyboardSize.height);
[scrollView setContentOffset:point animated:YES];
}
}
- (void)keyboardWasHidden:(NSNotification *)notif
{
UIEdgeInsets zeroInsets = UIEdgeInsetsZero;
UIScrollView *scrollView = (UIScrollView *)self.view;
[scrollView setContentInset:zeroInsets];
scrollView.scrollIndicatorInsets = zeroInsets;
}
Try this one:
self.automaticallyAdjustsScrollViewInsets = NO;
This is working for me...
it might be related to contentSize not working
except when set in VCs
- (void)viewDidLayoutSubviews
{
self.scrollView.contentSize = whatever
}
just saying you might be smashing your head against the wrong wall
So, just because I still found this answer useful, here is what I did. I took the advice of #alex-i and the comment by #yong-ho. But for some reason the height of the navigation bar wasn't quite enough.
UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.navigationController.navigationBar.frame.size.height + 20.0f, 0.0, 0.0, 0.0);
scrollView.contentInset = contentInsets;
scrollView.scrollIndicatorInsets = contentInsets;
Like I said, I had to add that 20.0f or my content was still cut off. Not sure why. If I figure it out I'll update my answer.
Set contentOffset to zero. It will work in all cases no matter your scroll view is inside navigation controller or any other. Find below the code snippet for same:
- (void)keyboardWasHidden:(NSNotification *)notif
{
UIScrollView *scrollView = (UIScrollView *)self.view;
scrollView.contentOffset = CGPoint.zero
}
I also found that if you set a new contentinset exactly the same as the existing inset, the scrollview may ignore it and revert to a zero inset. So an easy workaround is to check that the new contentinset you are setting is at least 1 point different than the last.

UITextView cursor below frame when changing frame

I have a UIViewCOntrollerthat contains a UITextView. When the keyboard appears I resize 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 rezise to the right size, but when the user types the cursor ends up "outside" the textView frame. See picture below:
The yellow area is the UITextView frame (I don't know what the blue line next to the R key is). I find this quite wired. I'm using iOS7 if that makes any difference.
Any ideas or tips?
Update
I have a UITextView subclass that draws horizontal lines with the following method (if that makes any difference):
- (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);
}
Instead of resizing the frame, why not give your text view a contentInset (and a matching scrollIndicatorInsets)? Remember that text views are actually scrollviews. This is the correct way to handle keyboard (or other) interference.
For more information on contentInset, see this question.
This seems to not be enough. Still use insets, as this is more correct (especially on iOS7, where the keyboard is transparent), but you will also need extra handling 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 better way of handling the caret, but this works.
All of the others answers I tried behaved somewhat strangely for me. Using an NSTimer to perform the scroll also meant that the user couldn't scroll up, since the caret would then end up off-screen and it would immediately scroll back down again. In the end I stuck with the original approach of changing the UITextView frame on the keyboard notification events, 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
A lot of answers already, I found that in my case it's actually much simpler. On keyboardWillShow I adjust the text view's contentInset and keep the frame full screen. And while scrollRangeToVisible: is not working for me like for so many others, the scroll view methods (from which UITextView inherits) work just fine. This works for me:
- (void)textViewDidChange:(UITextView *)textView
{
CGRect caret = [_textView caretRectForPosition:_textView.selectedTextRange.end];
[_textView scrollRectToVisible:caret animated:YES];
}
Anders and Leo Natan have great solutions. However, I needed to modify their answers a little to get the scrolling to work properly with contentInset. The problem I faced was that textViewDidBeginEditing: gets called before keyboardWasShown: so the contentInset change does not get reflected the first time through. 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
}
}
This is what I ended up doing, and something that seems 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];
}
A simpler solution to this problem is to update the text view frame in response to the textViewDidBegingEditing delegate method. For further details, see the following:
How to re-size UITextView when keyboard shown with iOS 7
For those that have a UITextView inside a UIScrollView where iOS < 7 took care of scrolling the caret into view: Here's how it works with iOS 7 (and also 5 & 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's 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 things missing are setting the UITextView's delegate and dismissing the keyboard.
Took 2-3 days to figure out what previously worked. Thanks, Apple.
Angel Naydenov's comment above is right, especially in cases such as switching from English to Japanese keyboard that shows suggests.
When switching keyboards, UIKeyboardWillShowNotification is called but UIKeyboardWillHideNotification is not called.
So you must adjust the inset to use the absolute value and not use +=.
Unrelatedly, [self.textView setContentOffset:newOffset animated:YES]; will not actually change the graphics in iOS 7.1 after the keyboard is shown for the second time, which is probably a bug. A workaround I used is replacing
[self.textView setContentOffset:newOffset animated:YES];
with
[UIView animateWithDuration:.25 animations:^{
self.textView.contentOffset = newOffset;
}];
Leo Natan, you started out well but your execution was relatively inefficient. Here is a better way of doing it 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];
}

Resources