Observe the notifications UIKeyboardWillShowNotification
and UIKeyboardWillHideNotification
, update the scrollView
content insets according to keyboard height, then scroll to the focused control.
- (void)viewDidLoad
{
[super viewDidLoad];
// register for keyboard notifications
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:self.view.window];
// register for keyboard notifications
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:self.view.window];
}
// Called when UIKeyboardWillShowNotification is sent
- (void)keyboardWillShow:(NSNotification*)notification
{
// if we have no view or are not visible in any window, we don't care
if (!self.isViewLoaded || !self.view.window) {
return;
}
NSDictionary *userInfo = [notification userInfo];
CGRect keyboardFrameInWindow;
[[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] getValue:&keyboardFrameInWindow];
// the keyboard frame is specified in window-level coordinates. this calculates the frame as if it were a subview of our view, making it a sibling of the scroll view
CGRect keyboardFrameInView = [self.view convertRect:keyboardFrameInWindow fromView:nil];
CGRect scrollViewKeyboardIntersection = CGRectIntersection(_scrollView.frame, keyboardFrameInView);
UIEdgeInsets newContentInsets = UIEdgeInsetsMake(0, 0, scrollViewKeyboardIntersection.size.height, 0);
// this is an old animation method, but the only one that retains compaitiblity between parameters (duration, curve) and the values contained in the userInfo-Dictionary.
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
_scrollView.contentInset = newContentInsets;
_scrollView.scrollIndicatorInsets = newContentInsets;
/*
* Depending on visual layout, _focusedControl should either be the input field (UITextField,..) or another element
* that should be visible, e.g. a purchase button below an amount text field
* it makes sense to set _focusedControl in delegates like -textFieldShouldBeginEditing: if you have multiple input fields
*/
if (_focusedControl) {
CGRect controlFrameInScrollView = [_scrollView convertRect:_focusedControl.bounds fromView:_focusedControl]; // if the control is a deep in the hierarchy below the scroll view, this will calculate the frame as if it were a direct subview
controlFrameInScrollView = CGRectInset(controlFrameInScrollView, 0, -10); // replace 10 with any nice visual offset between control and keyboard or control and top of the scroll view.
CGFloat controlVisualOffsetToTopOfScrollview = controlFrameInScrollView.origin.y - _scrollView.contentOffset.y;
CGFloat controlVisualBottom = controlVisualOffsetToTopOfScrollview + controlFrameInScrollView.size.height;
// this is the visible part of the scroll view that is not hidden by the keyboard
CGFloat scrollViewVisibleHeight = _scrollView.frame.size.height - scrollViewKeyboardIntersection.size.height;
if (controlVisualBottom > scrollViewVisibleHeight) { // check if the keyboard will hide the control in question
// scroll up until the control is in place
CGPoint newContentOffset = _scrollView.contentOffset;
newContentOffset.y += (controlVisualBottom - scrollViewVisibleHeight);
// make sure we don't set an impossible offset caused by the "nice visual offset"
// if a control is at the bottom of the scroll view, it will end up just above the keyboard to eliminate scrolling inconsistencies
newContentOffset.y = MIN(newContentOffset.y, _scrollView.contentSize.height - scrollViewVisibleHeight);
[_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
} else if (controlFrameInScrollView.origin.y < _scrollView.contentOffset.y) {
// if the control is not fully visible, make it so (useful if the user taps on a partially visible input field
CGPoint newContentOffset = _scrollView.contentOffset;
newContentOffset.y = controlFrameInScrollView.origin.y;
[_scrollView setContentOffset:newContentOffset animated:NO]; // animated:NO because we have created our own animation context around this code
}
}
[UIView commitAnimations];
}
// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillHide:(NSNotification*)notification
{
// if we have no view or are not visible in any window, we don't care
if (!self.isViewLoaded || !self.view.window) {
return;
}
NSDictionary *userInfo = notification.userInfo;
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:[[userInfo valueForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
[UIView setAnimationCurve:[[userInfo valueForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
// undo all that keyboardWillShow-magic
// the scroll view will adjust its contentOffset apropriately
_scrollView.contentInset = UIEdgeInsetsZero;
_scrollView.scrollIndicatorInsets = UIEdgeInsetsZero;
[UIView commitAnimations];
}