Move up keyboard when editing UITextField on iOS9 - ios

For my keyboards to move up to uncover UITextField in my iOS app, I used to implement this answer: https://stackoverflow.com/a/6908258/3855618 on iOS7 and 8 and it has worked perfectly for now. However on iOS 9.1, it doesn't work anymore.
To be more accurate, even if the background view does move up, the UITextField doesn't.
Any idea of what has changed so much since iOS9 and iOS 9.1?

The answer you have linked is not recommended. You should not set the view controller view's frame directly, especially not if you are using auto layout. Instead of changing the view's frame you should add a scrollview as a subview to the view, and adjust the content inset when the keyboard is shown or hidden.
From the official apple doc:
When asked to display the keyboard, the system slides it in from the bottom of the screen and positions it over your app’s content. Because it is placed on top of your content, it is possible for the keyboard to be placed on top of the text object that the user wanted to edit. When this happens, you must adjust your content so that the target object remains visible.
Adjusting your content typically involves temporarily resizing one or more views and positioning them so that the text object remains visible. The simplest way to manage text objects with the keyboard is to embed them inside a UIScrollView object (or one of its subclasses like UITableView). When the keyboard is displayed, all you have to do is reset the content area of the scroll view and scroll the desired text object into position. Thus, in response to a UIKeyboardDidShowNotification, your handler method would do the following:
Get the size of the keyboard.
Adjust the bottom content inset of your scroll view by the keyboard height.
Scroll the target text field into view.
// 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;
}

Zero lines of Code
Devoid of hacks, kludges, workaround and listeners.
The present question has been asked over and over since the dawn of iOS time. No answer on StackOverflow survived more than 2 iOS iterations. Rightly so, because the UIKit keeps changing from underneath your feet. There exists a design as opposed to implementation solution to this ancient problem. Use a UITableViewController.
Use a UITableViewController
When a UITableView is managed by a UITableViewController, the scrolling is managed automatically for you. Never tinker with UIKeyboardWillShowNotification, ever again. Merely create static or dynamic UITableViewCells to layout your interface, add UITextView or UITextField as needed ; merely becoming first responder will scroll the the proper location.
#availability(iOS, introduced=2.0)
Notes
Works on all iOS since 2.0.
Quote: «Waste no time optimizing a poor algorithm ; pick a better one»
See https://stackoverflow.com/a/32390936/218152.

We need to take keyboard frame from notification. When get reference of scrollView, tableView, etc. Convert low border of view to window`s coordinates. When determine how much keyboard covers our view, and if difference is greater than 0, we can add inset below.
Try this code:
- (void)subscribeKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)unsubscribeKeyboardNotifications
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIKeyboardWillHideNotification
object:nil];
}
- (void)keyboardWillShow:(NSNotification *)aNotification
{
CGRect keyBoardFrame = [[[aNotification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIWindow *keyWindow = [[[UIApplication sharedApplication] delegate] window];
UIScrollView *someScrollView = ......
CGPoint tableViewBottomPoint = CGPointMake(0, CGRectGetMaxY([someScrollView bounds]));
CGPoint convertedTableViewBottomPoint = [someScrollView convertPoint:tableViewBottomPoint
toView:keyWindow];
CGFloat keyboardOverlappedSpaceHeight = convertedTableViewBottomPoint.y - keyBoardFrame.origin.y;
if (keyboardOverlappedSpaceHeight > 0)
{
UIEdgeInsets tableViewInsets = UIEdgeInsetsMake(0, 0, keyboardOverlappedSpaceHeight, 0);
[someScrollView setContentInset:tableViewInsets];
}
}
- (void)keyboardWillHide:(NSNotification *)aNotification
{
UIEdgeInsets tableViewInsets = UIEdgeInsetsZero;
UIScrollView *someScrollView = ......
[someScrollView setContentInset:tableViewInsets];
}

Add all UITextField on UIScrollView and use TPKeyboardAvoiding

I'm usually listening to keyboard notifications and make according changes to layout constraints. See my other answer for more details and a sample project.

Try this code that I have used in my previous projects:
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
[self didBeginEditingIn:textField];
}
- (void)textFieldDidEndEditing:(UITextField *)textField
{
[self didEndEditing];
}
static const CGFloat KEYBOARD_ANIMATION_DURATION = 0.3;
static const CGFloat MINIMUM_SCROLL_FRACTION = 0.2;
static const CGFloat MAXIMUM_SCROLL_FRACTION = 0.8;
static const CGFloat PORTRAIT_KEYBOARD_HEIGHT = 216+100;
static const CGFloat LANDSCAPE_KEYBOARD_HEIGHT = 162+100;
- (void)didBeginEditingIn:(UIView *)view
{
CGRect textFieldRect = [self.view.window convertRect:view.bounds fromView:view];
CGRect viewRect = [self.view.window convertRect:self.view.bounds fromView:self.view];
CGFloat midline = textFieldRect.origin.y + 0.5* textFieldRect.size.height;
CGFloat numerator = midline - viewRect.origin.y- MINIMUM_SCROLL_FRACTION * viewRect.size.height;
CGFloat denominator = (MAXIMUM_SCROLL_FRACTION - MINIMUM_SCROLL_FRACTION)* viewRect.size.height;
CGFloat heightFraction = numerator / denominator;
if (heightFraction < 0.0)
{
heightFraction = 0.0;
}
else if (heightFraction > 1.0)
{
heightFraction = 1.0;
}
UIInterfaceOrientation orientation =
[[UIApplication sharedApplication] statusBarOrientation];
if (orientation == UIInterfaceOrientationPortrait ||
orientation == UIInterfaceOrientationPortraitUpsideDown)
{
_animatedDistance = floor(PORTRAIT_KEYBOARD_HEIGHT * heightFraction);
}
else
{
_animatedDistance = floor(LANDSCAPE_KEYBOARD_HEIGHT * heightFraction);
}
CGRect viewFrame = self.view.frame;
viewFrame.origin.y -= _animatedDistance;
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:KEYBOARD_ANIMATION_DURATION];
[self.view setFrame:viewFrame];
[UIView commitAnimations];
}
- (void)didEndEditing
{
CGRect viewFrame = self.view.frame;
viewFrame.origin.y += _animatedDistance;
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:KEYBOARD_ANIMATION_DURATION];
[self.view setFrame:viewFrame];
[UIView commitAnimations];
}

i followed the doc from #Istvan to the apple site, and there are a lot of stuff missing to make it work:
1. Set your .h document to <UITextFieldDelegate> (to be able to work with "activefield")
2. In the viewDidLoad, set the delegates to your UITextfields, and set the height of your scrollview content with a bigger height (in my case i've setted 500 more):
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenWidth = screenRect.size.width;
CGFloat screenHeight = screenRect.size.height + 500;
_scrollView.contentSize = CGSizeMake(screenWidth, screenHeight);
And now it's all working...

Related

Textview Frame is become squeez and cannot come back to its original form?

keyboardWillShow method implementation :
- (void)keyboardWillShow:(NSNotification *)notification
{
[UIView beginAnimations:nil context:nil];
CGRect endRect = [[notification.userInfo
objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect newRect = txtNotes.frame;
//Down size your text view
newRect.size.height -= endRect.size.height;
txtNotes.frame = newRect;
[UIView commitAnimations];
}
keyboardWillHide method implementation :
- (void)keyboardWillHide:(NSNotification *)notification
{
// Resize your textview when keyboard is going to hide
UIEdgeInsets contentInsets = UIEdgeInsetsZero;
txtNotes.contentInset = contentInsets;
txtNotes.scrollIndicatorInsets = contentInsets;
}
Couple of things:
1) META: You should add 4 spaces before each of the code lines to get the code to display properly.
2) This looks like a basic constraints issue. You should examine the Auto Layout of your storyboard.

UITextView how to keep cursor above keyboard

I have a ViewController with UITextView taking up the whole view, with a navigation bar on top. Almost like Apple's "Notes" app. What I'm trying to achieve is to keep the textview's cursor visible when editing start, or when editing.
I was able to get the cursor's CGPoint, but I'm having difficult time calculating the scroll point. How can I achieve this?
Thanks
When textview starts editing
- (void)keyboardDidShow:(NSNotification*)aNotification {
// Keyboard
NSDictionary *info = [aNotification userInfo];
CGRect keyPadFrame = [[UIApplication sharedApplication].keyWindow convertRect:[[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:self.view];
CGSize keyboardSize = keyPadFrame.size;
kbSize = keyboardSize;
[self scrollToCursor];
}
When textview is editing
- (void)textViewDidChange:(UITextView *)textView {
// Scroll to cursor
[self scrollToCursor];
}
Scroll to cursor method
- (void)scrollToCursor {
// View
CGRect viewBounds = self.view.bounds;
CGRect visibleViewBounds = CGRectMake(viewBounds.origin.x,
viewBounds.origin.y + (self.navigationController.navigationBar.frame.size.height + [UIApplication sharedApplication].statusBarFrame.size.height),
viewBounds.size.width,
viewBounds.size.height - (kbSize.height + self.navigationController.navigationBar.frame.size.height + [UIApplication sharedApplication].statusBarFrame.size.height));
// TextView
CGPoint textViewOrigin = [self.view convertRect:self.noteTextView.frame fromView:self.noteTextView.superview].origin;
// Cursor
CGPoint textViewCursor = [self.noteTextView caretRectForPosition:self.noteTextView.selectedTextRange.start].origin;
CGPoint cursorPoint = CGPointMake((textViewCursor.x + textViewOrigin.x), (textViewCursor.y - self.noteTextView.contentOffset.y));
// Scroll to point
if (!CGRectContainsPoint(visibleViewBounds, CGPointMake(cursorPoint.x, cursorPoint.y + 25/*25 for cursor's height*/))) {
[self.noteTextView setContentOffset:CGPointMake(0, 0)/*How to calculate??*/ animated:YES];
}
}
This is not tested at all, but would be my first attempt at it.
Get the keyboard height by listening to the KeyboardWillChangeFrameNotification
CGRect keyboardFrame;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillChange:) name:UIKeyboardWillChangeFrameNotification object:nil];
- (void)keyboardWillChange:(NSNotification *)notification {
keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
keyboardFrame = [self.view convertRect:keyboardRect fromView:nil];
}
That will give you the keyboard height.
Then get the screen height:
CGRect screenBound = [[UIScreen mainScreen] bounds];
CGSize screenSize = screenBound.size;
CGFloat screenHeight = screenSize.height;
Then if you know the CGPoint of the cursor, do something like this:
CGFloat keyboardTop = (screenHeight - (keyboardFrame.size.height + <padding if you want it>));
if (currentCursorPosition.y > keyboardTop)
{
[self.noteTextView setContentOffset:CGPointMake(0, (cursorPoint.y - (viewBounds.size.height - kbSize.height)) + self.noteTextView.contentOffset.y + 25);
}
Ideally this should keep the cursor at the top of the keyboard and then scroll as you move the cursor down.
For your sanity, just use a library.
Here is a good one: https://github.com/hackiftekhar/IQKeyboardManager
Carthage:
github "hackiftekhar/IQKeyboardManager"
CocoaPods:
pod 'IQKeyboardManagerSwift', '6.3.0'
or
pod 'IQKeyboardManager', '3.3.7' #iOS7

contentInset being ignored in ios7 for UIScrollView

This worked before ios7 when someone tapped on anything that could become first responder inside a UIScrollView. Now it does not - UITextFields/Views still can show under the keyboard.
Code:
- (void)keyboardWasShown:(NSNotification*)notification{
//Some similar questions mentioned this might work, but made no difference for me
self.automaticallyAdjustsScrollViewInsets=NO;
NSDictionary* info = [notification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
float height = 0.0;
if (UIDeviceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
height = kbSize.width;
} else {
height = kbSize.height;
}
UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, height, 0.0);
[UIView animateWithDuration:.25
delay:0
options:(UIViewAnimationOptionAllowUserInteraction)
animations:^
{
self.editorScrollView.contentInset = contentInsets;
self.editorScrollView.scrollIndicatorInsets = contentInsets;
}
completion:^(BOOL finished)
{
}];
}
Currently, with this code nothing takes place when a uitextfield/view is assigned first responder status. The insets don't seem to change - I perhaps could use contentOffset but I would have to find the origin view's Y who just become first responder to do that.
Like I said, before ios7 this code worked (no textfield/view would be hidden behind the keyboard when assigned first responder status). I seem to be missing something obvious or perhaps there is a better way of doing this in ios7?
A better way to detect keyboard changing and frame.
The key point is to convert keyboard frame: CGRect keyboardFrameInsideView = [self.view convertRect:keyboardFrame fromView:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(keyboardFrameWillChange:)
name:UIKeyboardWillShowNotification
object:nil];
- (void)keyboardFrameWillChange:(NSNotification *)notification
{
CGRect keyboardFrame;
[[notification.userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrame];
CGRect keyboardFrameInsideView = [self.view convertRect:keyboardFrame fromView:nil];
CGRect r = self.bodyView.frame;
r.size.height = CGRectGetMinY(keyboardFrameInsideView) - r.origin.y;
self.bodyView.frame = r;
}

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

iOS: Keep active UITextField in middle of the Screen while Keyboard is shown

I have a UITableViewController with some static cells (filled with UITextFields). Now I'd like to keep the active UITextField in middle of my Screen (even if I go one Cell/UITextfield up/down).
How can I achieve this?
I did this a couple of times in the past, didn't like it very much though.
My solution was to move the view with the table up, so the selected cell can stay visible.
First I added some observers to know when the keyboard appeared or disappeared
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
Then I did something like this, to push the table up:
- (void)keyboardWillShowNotification:(NSNotification*)notification {
CGSize kbSize = [[notification.userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
double animationDuration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGPoint newCenter = self.myable.center;
NSLog(#"Height: %f Width: %f", kbSize.height, kbSize.width);
// Portrait: Height: 264.000000 Width: 768.000000
// Landscape: Height: 1024.000000 Width: 352.000000
if([[UIApplication sharedApplication] statusBarOrientation] < 3) {
newCenter = CGPointMake(newCenter.x, heightP - kbSize.height - self.myTable.frame.size.height/2);
}
else {
newCenter = CGPointMake(newCenter.x, heightL - kbSize.width - self.myTable.frame.size.height/2);
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:animationDuration];
self.myTable.center = newCenter;
[UIView commitAnimations];
}
- (void)keyboardWillHideNotification:(NSNotification*)notification {
NSLog(#"keyboard disappeared");
double animationDuration = [[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGPoint newCenter = self.myTable.center;
if([[UIApplication sharedApplication] statusBarOrientation] < 3) {
newCenter = CGPointMake(newCenter.x, 0 + self.lmyTable.frame.size.height/2);
}
else {
newCenter = CGPointMake(newCenter.x, 0 + self.myTable.frame.size.height/2);
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:animationDuration];
self.myTable.center = newCenter;
[UIView commitAnimations];
}
Basically, I read the keyboard height and added it to the center of the table, to push it upwards, and then removed the same height when the keyboard disappeared.
Now, there is a catch!
I removed it from my code because it was very specific, but you have to be careful to move your table only when the keyboard would cover your cell! Otherwise you will end up pushing visible cells over the top of the screen.
This pretty much depends on your view setting, the borders, the size of the table and so on, so I can't help you on this part, but I hope you got the basic idea!
Good luck
Even simpler... in your tableview's delegate didSelectRowAtIndexPathuse scrollToRowAtIndexPath:<#(NSIndexPath *)#> atScrollPosition:<#(UITableViewScrollPosition)#> animated:<#(BOOL)#> to scroll to your cell. You might have to change your tableview's frame for this and change it back again when the cell gets deselected. Or us it's scroll insets property to scroll.

Resources