//
|
// 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 <objc/runtime.h>
|
|
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<UIScrollViewDelegate>)self)) {
|
[(UIScrollView *)view setDelegate:(id<UIScrollViewDelegate>)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
|