linphone-iphone/Classes/Utils/TPKeyboardAvoiding/UIScrollView+TPKeyboardAvoidingAdditions.m

285 lines
12 KiB
Objective-C
Executable file

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