// // TPKeyboardAvoidingScrollView.m // // Created by Michael Tyson on 11/04/2011. // Copyright 2011 A Tasty Pixel. All rights reserved. // #import "TPKeyboardAvoidingScrollView.h" #define _UIKeyboardFrameEndUserInfoKey (&UIKeyboardFrameEndUserInfoKey != NULL ? UIKeyboardFrameEndUserInfoKey : @"UIKeyboardBoundsUserInfoKey") @interface TPKeyboardAvoidingScrollView () - (UIView*)findFirstResponderBeneathView:(UIView*)view; - (UIEdgeInsets)contentInsetForKeyboard; - (CGFloat)idealOffsetForView:(UIView *)view withSpace:(CGFloat)space; - (CGRect)keyboardRect; @end @implementation TPKeyboardAvoidingScrollView - (void)setup { _priorInsetSaved = NO; if ( CGSizeEqualToSize(self.contentSize, CGSizeZero) ) { self.contentSize = self.bounds.size; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } -(id)initWithFrame:(CGRect)frame { if ( !(self = [super initWithFrame:frame]) ) return nil; [self setup]; return self; } -(void)awakeFromNib { [self setup]; } -(void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; #if !__has_feature(objc_arc) [super dealloc]; #endif } -(void)setFrame:(CGRect)frame { [super setFrame:frame]; CGSize contentSize = _originalContentSize; contentSize.width = MAX(contentSize.width, self.frame.size.width); contentSize.height = MAX(contentSize.height, self.frame.size.height); [super setContentSize:contentSize]; if ( _keyboardVisible ) { self.contentInset = [self contentInsetForKeyboard]; } } -(void)setContentSize:(CGSize)contentSize { _originalContentSize = contentSize; contentSize.width = MAX(contentSize.width, self.frame.size.width); contentSize.height = MAX(contentSize.height, self.frame.size.height); [super setContentSize:contentSize]; if ( _keyboardVisible ) { self.contentInset = [self contentInsetForKeyboard]; } } - (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [[self findFirstResponderBeneathView:self] resignFirstResponder]; [super touchesEnded:touches withEvent:event]; } - (void)keyboardWillShow:(NSNotification*)notification { _keyboardRect = [[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue]; _keyboardVisible = YES; UIView *firstResponder = [self findFirstResponderBeneathView:self]; if ( !firstResponder ) { // No child view is the first responder - nothing to do here return; } if (!_priorInsetSaved) { _priorInset = self.contentInset; _priorInsetSaved = YES; } // Shrink view's inset by the keyboard's height, and scroll to show the text field/view being edited [UIView beginAnimations:nil context:NULL]; [UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]]; [UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]]; self.contentInset = [self contentInsetForKeyboard]; [self setContentOffset:CGPointMake(self.contentOffset.x, [self idealOffsetForView:firstResponder withSpace:[self keyboardRect].origin.y - self.bounds.origin.y]) animated:YES]; [self setScrollIndicatorInsets:self.contentInset]; [UIView commitAnimations]; } - (void)keyboardWillHide:(NSNotification*)notification { _keyboardRect = CGRectZero; _keyboardVisible = NO; // Restore dimensions to prior size [UIView beginAnimations:nil context:NULL]; [UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]]; [UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]]; self.contentInset = _priorInset; [self setScrollIndicatorInsets:self.contentInset]; _priorInsetSaved = NO; [UIView commitAnimations]; } - (UIView*)findFirstResponderBeneathView:(UIView*)view { // Search recursively for first responder for ( UIView *childView in view.subviews ) { if ( [childView respondsToSelector:@selector(isFirstResponder)] && [childView isFirstResponder] ) return childView; UIView *result = [self findFirstResponderBeneathView:childView]; if ( result ) return result; } return nil; } - (UIEdgeInsets)contentInsetForKeyboard { UIEdgeInsets newInset = self.contentInset; CGRect keyboardRect = [self keyboardRect]; newInset.bottom = keyboardRect.size.height - ((keyboardRect.origin.y+keyboardRect.size.height) - (self.bounds.origin.y+self.bounds.size.height)); return newInset; } -(CGFloat)idealOffsetForView:(UIView *)view withSpace:(CGFloat)space { // Convert the rect to get the view's distance from the top of the scrollView. CGRect rect = [view convertRect:view.bounds toView:self]; // Set starting offset to that point CGFloat offset = rect.origin.y; if ( self.contentSize.height - offset < space ) { // Scroll to the bottom offset = self.contentSize.height - space; } else { if ( view.bounds.size.height < space ) { // Center vertically if there's room offset -= floor((space-view.bounds.size.height)/2.0); } if ( offset + space > self.contentSize.height ) { // Clamp to content size offset = self.contentSize.height - space; } } if (offset < 0) offset = 0; return offset; } -(void)adjustOffsetToIdealIfNeeded { // Only do this if the keyboard is already visible if ( !_keyboardVisible ) return; CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom; CGPoint idealOffset = CGPointMake(0, [self idealOffsetForView:[self findFirstResponderBeneathView:self] withSpace:visibleSpace]); [self setContentOffset:idealOffset animated:YES]; } - (CGRect)keyboardRect { CGRect keyboardRect = [self convertRect:_keyboardRect fromView:nil]; if ( keyboardRect.origin.y == 0 ) { CGRect screenBounds = [self convertRect:[UIScreen mainScreen].bounds fromView:nil]; keyboardRect.origin = CGPointMake(0, screenBounds.size.height - keyboardRect.size.height); } return keyboardRect; } @end