// // UIScrollView+TPKeyboardAvoidingAdditions.m // TPKeyboardAvoidingSample // // Created by Michael Tyson on 30/09/2013. // Copyright 2013 A Tasty Pixel. All rights reserved. // #import "UIScrollView+TPKeyboardAvoidingAdditions.h" #import "TPKeyboardAvoidingScrollView.h" #import static const CGFloat kCalculatedContentPadding = 10; static const CGFloat kMinimumScrollOffsetPadding = 20; static const int kStateKey; #define _UIKeyboardFrameEndUserInfoKey (&UIKeyboardFrameEndUserInfoKey != NULL ? UIKeyboardFrameEndUserInfoKey : @"UIKeyboardBoundsUserInfoKey") @interface TPKeyboardAvoidingState : NSObject @property (nonatomic, assign) UIEdgeInsets priorInset; @property (nonatomic, assign) UIEdgeInsets priorScrollIndicatorInsets; @property (nonatomic, assign) BOOL keyboardVisible; @property (nonatomic, assign) CGRect keyboardRect; @property (nonatomic, assign) CGSize priorContentSize; @end @implementation UIScrollView (TPKeyboardAvoidingAdditions) - (TPKeyboardAvoidingState*)keyboardAvoidingState { TPKeyboardAvoidingState *state = objc_getAssociatedObject(self, &kStateKey); if ( !state ) { state = [[TPKeyboardAvoidingState alloc] init]; objc_setAssociatedObject(self, &kStateKey, state, OBJC_ASSOCIATION_RETAIN_NONATOMIC); #if !__has_feature(objc_arc) [state release]; #endif } return state; } - (void)TPKeyboardAvoiding_keyboardWillShow:(NSNotification*)notification { TPKeyboardAvoidingState *state = self.keyboardAvoidingState; if ( state.keyboardVisible ) { return; } UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self]; state.keyboardRect = [self convertRect:[[[notification userInfo] objectForKey:_UIKeyboardFrameEndUserInfoKey] CGRectValue] fromView:nil]; state.keyboardVisible = YES; state.priorInset = self.contentInset; state.priorScrollIndicatorInsets = self.scrollIndicatorInsets; if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) { state.priorContentSize = self.contentSize; if ( CGSizeEqualToSize(self.contentSize, CGSizeZero) ) { // Set the content size, if it's not set. Do not set content size explicitly if auto-layout // is being used to manage subviews self.contentSize = [self TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames]; } } // 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 TPKeyboardAvoiding_contentInsetForKeyboard]; if ( firstResponder ) { CGFloat viewableHeight = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom; [self setContentOffset:CGPointMake(self.contentOffset.x, [self TPKeyboardAvoiding_idealOffsetForView:firstResponder withViewingAreaHeight:viewableHeight]) animated:NO]; } self.scrollIndicatorInsets = self.contentInset; [UIView commitAnimations]; } - (void)TPKeyboardAvoiding_keyboardWillHide:(NSNotification*)notification { TPKeyboardAvoidingState *state = self.keyboardAvoidingState; if ( !state.keyboardVisible ) { return; } state.keyboardRect = CGRectZero; state.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]]; if ( [self isKindOfClass:[TPKeyboardAvoidingScrollView class]] ) { self.contentSize = state.priorContentSize; } self.contentInset = state.priorInset; self.scrollIndicatorInsets = state.priorScrollIndicatorInsets; [UIView commitAnimations]; } - (void)TPKeyboardAvoiding_updateContentInset { TPKeyboardAvoidingState *state = self.keyboardAvoidingState; if ( state.keyboardVisible ) { self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard]; } } - (void)TPKeyboardAvoiding_updateFromContentSizeChange { TPKeyboardAvoidingState *state = self.keyboardAvoidingState; if ( state.keyboardVisible ) { state.priorContentSize = self.contentSize; self.contentInset = [self TPKeyboardAvoiding_contentInsetForKeyboard]; } } #pragma mark - Utilities - (BOOL)TPKeyboardAvoiding_focusNextTextField { UIView *firstResponder = [self TPKeyboardAvoiding_findFirstResponderBeneathView:self]; if ( !firstResponder ) { return NO; } CGFloat minY = CGFLOAT_MAX; UIView *view = nil; [self TPKeyboardAvoiding_findTextFieldAfterTextField:firstResponder beneathView:self minY:&minY foundView:&view]; if ( view ) { [view performSelector:@selector(becomeFirstResponder) withObject:nil afterDelay:0.0]; return YES; } return NO; } -(void)TPKeyboardAvoiding_scrollToActiveTextField { TPKeyboardAvoidingState *state = self.keyboardAvoidingState; if ( !state.keyboardVisible ) return; CGFloat visibleSpace = self.bounds.size.height - self.contentInset.top - self.contentInset.bottom; CGPoint idealOffset = CGPointMake(0, [self TPKeyboardAvoiding_idealOffsetForView:[self TPKeyboardAvoiding_findFirstResponderBeneathView:self] withViewingAreaHeight:visibleSpace]); // Ordinarily we'd use -setContentOffset:animated:YES here, but it does not appear to // scroll to the desired content offset. So we wrap in our own animation block. [UIView animateWithDuration:0.25 animations:^{ [self setContentOffset:idealOffset animated:NO]; }]; } #pragma mark - Helpers - (UIView*)TPKeyboardAvoiding_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 TPKeyboardAvoiding_findFirstResponderBeneathView:childView]; if ( result ) return result; } return nil; } - (void)TPKeyboardAvoiding_findTextFieldAfterTextField:(UIView*)priorTextField beneathView:(UIView*)view minY:(CGFloat*)minY foundView:(UIView**)foundView { // Search recursively for text field or text view below priorTextField CGFloat priorFieldOffset = CGRectGetMinY([self convertRect:priorTextField.frame fromView:priorTextField.superview]); for ( UIView *childView in view.subviews ) { if ( childView.hidden ) continue; if ( ([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]]) && childView.isUserInteractionEnabled) { CGRect frame = [self convertRect:childView.frame fromView:view]; if ( childView != priorTextField && CGRectGetMinY(frame) >= priorFieldOffset && CGRectGetMinY(frame) < *minY && !(frame.origin.y == priorTextField.frame.origin.y && frame.origin.x < priorTextField.frame.origin.x) ) { *minY = CGRectGetMinY(frame); *foundView = childView; } } else { [self TPKeyboardAvoiding_findTextFieldAfterTextField:priorTextField beneathView:childView minY:minY foundView:foundView]; } } } - (void)TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:(UIView*)view { for ( UIView *childView in view.subviews ) { if ( ([childView isKindOfClass:[UITextField class]] || [childView isKindOfClass:[UITextView class]]) ) { [self TPKeyboardAvoiding_initializeView:childView]; } else { [self TPKeyboardAvoiding_assignTextDelegateForViewsBeneathView:childView]; } } } -(CGSize)TPKeyboardAvoiding_calculatedContentSizeFromSubviewFrames { BOOL wasShowingVerticalScrollIndicator = self.showsVerticalScrollIndicator; BOOL wasShowingHorizontalScrollIndicator = self.showsHorizontalScrollIndicator; self.showsVerticalScrollIndicator = NO; self.showsHorizontalScrollIndicator = NO; CGRect rect = CGRectZero; for ( UIView *view in self.subviews ) { rect = CGRectUnion(rect, view.frame); } rect.size.height += kCalculatedContentPadding; self.showsVerticalScrollIndicator = wasShowingVerticalScrollIndicator; self.showsHorizontalScrollIndicator = wasShowingHorizontalScrollIndicator; return rect.size; } - (UIEdgeInsets)TPKeyboardAvoiding_contentInsetForKeyboard { TPKeyboardAvoidingState *state = self.keyboardAvoidingState; UIEdgeInsets newInset = self.contentInset; CGRect keyboardRect = state.keyboardRect; newInset.bottom = keyboardRect.size.height - (CGRectGetMaxY(keyboardRect) - CGRectGetMaxY(self.bounds)); return newInset; } -(CGFloat)TPKeyboardAvoiding_idealOffsetForView:(UIView *)view withViewingAreaHeight:(CGFloat)viewAreaHeight { CGSize contentSize = self.contentSize; CGFloat offset = 0.0; CGRect subviewRect = [view convertRect:view.bounds toView:self]; // Attempt to center the subview in the visible space, but if that means there will be less than kMinimumScrollOffsetPadding // pixels above the view, then substitute kMinimumScrollOffsetPadding CGFloat padding = (viewAreaHeight - subviewRect.size.height) / 2; if ( padding < kMinimumScrollOffsetPadding ) { padding = kMinimumScrollOffsetPadding; } // Ideal offset places the subview rectangle origin "padding" points from the top of the scrollview. // If there is a top contentInset, also compensate for this so that subviewRect will not be placed under // things like navigation bars. offset = subviewRect.origin.y - padding - self.contentInset.top; // Constrain the new contentOffset so we can't scroll past the bottom. Note that we don't take the bottom // inset into account, as this is manipulated to make space for the keyboard. if ( offset > (contentSize.height - viewAreaHeight) ) { offset = contentSize.height - viewAreaHeight; } // Constrain the new contentOffset so we can't scroll past the top, taking contentInsets into account if ( offset < -self.contentInset.top ) { offset = -self.contentInset.top; } return offset; } - (void)TPKeyboardAvoiding_initializeView:(UIView*)view { if ( [view isKindOfClass:[UITextField class]] && ((UITextField*)view).returnKeyType == UIReturnKeyDefault && (![(id)view delegate] || [(UIScrollView*)view delegate] == (id)self) ) { [(UIScrollView*)view setDelegate:(id)self]; UIView *otherView = nil; CGFloat minY = CGFLOAT_MAX; [self TPKeyboardAvoiding_findTextFieldAfterTextField:view beneathView:self minY:&minY foundView:&otherView]; if ( otherView ) { ((UITextField*)view).returnKeyType = UIReturnKeyNext; } else { ((UITextField*)view).returnKeyType = UIReturnKeyDone; } } } @end @implementation TPKeyboardAvoidingState @end