// // 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