Files
IQKeyboardManager/IQKeyBoardManager/IQKeyboardManager.m
2015-04-19 20:52:38 +05:30

1736 lines
74 KiB
Objective-C
Executable File

//
// IQKeyboardManager.m
// https://github.com/hackiftekhar/IQKeyboardManager
// Copyright (c) 2013-15 Iftekhar Qurashi.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
#import "IQKeyboardManager.h"
#import "IQUIView+Hierarchy.h"
#import "IQUIView+IQKeyboardToolbar.h"
#import "IQUIWindow+Hierarchy.h"
#import "IQNSArray+Sort.h"
#import "IQToolbar.h"
#import "IQBarButtonItem.h"
#import "IQKeyboardManagerConstantsInternal.h"
#import <UIKit/UINavigationBar.h>
#import <UIKit/UITapGestureRecognizer.h>
#import <UIKit/UITextField.h>
#import <UIKit/UITextView.h>
#import <UIKit/UITableViewController.h>
#import <UIKit/UINavigationController.h>
#import <UIKit/UITableView.h>
#import <UIKit/UITouch.h>
#ifdef NSFoundationVersionNumber_iOS_5_1
#import <UIKit/UICollectionView.h>
#import <UIKit/NSLayoutConstraint.h>
#endif
NSInteger const kIQDoneButtonToolbarTag = -1002;
NSInteger const kIQPreviousNextButtonToolbarTag = -1005;
void _IQShowLog(NSString *logString);
@interface IQKeyboardManager()<UIGestureRecognizerDelegate>
// Private helper methods
- (void)adjustFrame;
// Private function to manipulate RootViewController's frame with animation.
- (void)setRootViewFrame:(CGRect)frame;
// Keyboard Notification methods
- (void)keyboardWillShow:(NSNotification*)aNotification;
- (void)keyboardWillHide:(NSNotification*)aNotification;
- (void)keyboardDidHide:(NSNotification*)aNotification;
// UITextField/UITextView Notification methods
- (void)textFieldViewDidBeginEditing:(NSNotification*)notification;
- (void)textFieldViewDidEndEditing:(NSNotification*)notification;
- (void)textFieldViewDidChange:(NSNotification*)notification;
// Rotation notification
- (void)willChangeStatusBarOrientation:(NSNotification*)aNotification;
// Tap Recognizer
- (void)tapRecognized:(UITapGestureRecognizer*)gesture;
// Next/Previous/Done methods
-(void)previousAction:(id)segmentedControl;
-(void)nextAction:(id)segmentedControl;
-(void)doneAction:(IQBarButtonItem*)barButton;
// Adding Removing IQToolbar methods
- (void)addToolbarIfRequired;
- (void)removeToolbarIfRequired;
@end
@implementation IQKeyboardManager
{
@package
/*******************************************/
/** To save UITextField/UITextView object voa textField/textView notifications. */
__weak UIView *_textFieldView;
/** used with canAdjustTextView boolean. */
__block CGRect _textFieldViewIntialFrame;
/** To save rootViewController.view.frame. */
CGRect _topViewBeginRect;
/** To save rootViewController */
__weak UIViewController *_rootViewController;
/*******************************************/
/** Variable to save lastScrollView that was scrolled. */
__weak UIScrollView *_lastScrollView;
/** LastScrollView's initial contentInsets. */
UIEdgeInsets _startingContentInsets;
/** LastScrollView's initial contentOffset. */
CGPoint _startingContentOffset;
/*******************************************/
/** To save keyboardWillShowNotification. Needed for enable keyboard functionality. */
NSNotification *_kbShowNotification;
/** To save keyboard size. */
CGSize _kbSize;
/** To save keyboard animation duration. */
CGFloat _animationDuration;
/** To mimic the keyboard animation */
NSInteger _animationCurve;
/*******************************************/
/** TapGesture to resign keyboard on view's touch. */
UITapGestureRecognizer *_tapGesture;
/*******************************************/
/** Default toolbar tintColor to be used within the project. Default is black. */
UIColor *_defaultToolbarTintColor;
/*******************************************/
/** Set of restricted classes for library */
NSMutableSet *_disabledClasses;
/** Set of restricted classes for adding toolbar */
NSMutableSet *_disabledToolbarClasses;
/** Set of permitted classes to add all inner textField as siblings */
NSMutableSet *_toolbarPreviousNextConsideredClass;
/*******************************************/
struct {
/** used with canAdjustTextView to detect a textFieldView frame is changes or not. (Bug ID: #92)*/
unsigned int isTextFieldViewFrameChanged:1;
/** Boolean to maintain keyboard is showing or it is hide. To solve rootViewController.view.frame calculations. */
unsigned int isKeyboardShowing:1;
} _keyboardManagerFlags;
}
//UIKeyboard handling
@synthesize enable = _enable;
@synthesize keyboardDistanceFromTextField = _keyboardDistanceFromTextField;
@synthesize preventShowingBottomBlankSpace = _preventShowingBottomBlankSpace;
//Keyboard Appearance handling
@synthesize overrideKeyboardAppearance = _overrideKeyboardAppearance;
@synthesize keyboardAppearance = _keyboardAppearance;
//IQToolbar handling
@synthesize enableAutoToolbar = _enableAutoToolbar;
@synthesize toolbarManageBehaviour = _toolbarManageBehaviour;
#ifdef NSFoundationVersionNumber_iOS_6_1
@synthesize shouldToolbarUsesTextFieldTintColor = _shouldToolbarUsesTextFieldTintColor;
#endif
@synthesize shouldShowTextFieldPlaceholder = _shouldShowTextFieldPlaceholder;
@synthesize placeholderFont = _placeholderFont;
//TextView handling
@synthesize canAdjustTextView = _canAdjustTextView;
@synthesize shouldFixTextViewClip = _shouldFixTextViewClip;
//Resign handling
@synthesize shouldResignOnTouchOutside = _shouldResignOnTouchOutside;
//Sound handling
@synthesize shouldPlayInputClicks = _shouldPlayInputClicks;
//Animation handling
@synthesize shouldAdoptDefaultKeyboardAnimation = _shouldAdoptDefaultKeyboardAnimation;
//ScrollView handling
@synthesize shouldRestoreScrollViewContentOffset= _shouldRestoreScrollViewContentOffset;
#pragma mark - Initializing functions
/** Override +load method to enable KeyboardManager when class loader load IQKeyboardManager. Enabling when app starts (No need to write any code) */
+(void)load
{
[super load];
//Enabling IQKeyboardManager.
[[IQKeyboardManager sharedManager] setEnable:YES];
}
/* Singleton Object Initialization. */
-(instancetype)init
{
if (self = [super init])
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Registering for keyboard notification.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
// Registering for textField notification.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidBeginEditing:) name:UITextFieldTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
// Registering for textView notification.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidChange:) name:UITextViewTextDidChangeNotification object:nil];
// Registering for orientation changes notification
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willChangeStatusBarOrientation:) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil];
//Creating gesture for @shouldResignOnTouchOutside. (Enhancement ID: #14)
_tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRecognized:)];
[_tapGesture setDelegate:self];
// Default settings
[self setKeyboardDistanceFromTextField:10.0];
_animationDuration = 0.25;
//Setting it's initial values
_enable = NO;
_defaultToolbarTintColor = [UIColor blackColor];
[self setCanAdjustTextView:NO];
[self setShouldPlayInputClicks:NO];
[self setShouldResignOnTouchOutside:NO];
[self setOverrideKeyboardAppearance:NO];
[self setKeyboardAppearance:UIKeyboardAppearanceDefault];
[self setEnableAutoToolbar:YES];
[self setShouldFixTextViewClip:YES];
[self setPreventShowingBottomBlankSpace:YES];
[self setShouldShowTextFieldPlaceholder:YES];
[self setShouldAdoptDefaultKeyboardAnimation:YES];
[self setShouldRestoreScrollViewContentOffset:NO];
[self setToolbarManageBehaviour:IQAutoToolbarBySubviews];
//Initializing disabled classes Set.
_disabledClasses = [[NSMutableSet alloc] initWithObjects:[UITableViewController class], nil];
_disabledToolbarClasses = [[NSMutableSet alloc] init];
#ifdef NSFoundationVersionNumber_iOS_6_1
[self setShouldToolbarUsesTextFieldTintColor:NO];
#endif
#ifdef NSFoundationVersionNumber_iOS_5_1
_toolbarPreviousNextConsideredClass = [[NSMutableSet alloc] initWithObjects:[UITableView class],[UICollectionView class], nil];
#else
_toolbarPreviousNextConsideredClass = [[NSMutableSet alloc] initWithObjects:[UITableView class], nil];
#endif
});
}
return self;
}
/* Automatically called from the `+(void)load` method. */
+ (instancetype)sharedManager
{
//Singleton instance
static IQKeyboardManager *kbManager;
//Dispatching it once.
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Initializing keyboard manger.
kbManager = [[self alloc] init];
});
//Returning kbManager.
return kbManager;
}
#pragma mark - Dealloc
-(void)dealloc
{
// Disable the keyboard manager.
[self setEnable:NO];
//Removing notification observers on dealloc.
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Property functions
-(void)setEnable:(BOOL)enable
{
// If not enabled, enable it.
if (enable == YES && _enable == NO)
{
//Setting NO to _enable.
_enable = enable;
//If keyboard is currently showing. Sending a fake notification for keyboardWillShow to adjust view according to keyboard.
if (_kbShowNotification) [self keyboardWillShow:_kbShowNotification];
_IQShowLog(IQLocalizedString(@"enabled", nil));
}
//If not disable, desable it.
else if (enable == NO && _enable == YES)
{
//Sending a fake notification for keyboardWillHide to retain view's original frame.
[self keyboardWillHide:nil];
//Setting NO to _enable.
_enable = enable;
_IQShowLog(IQLocalizedString(@"disabled", nil));
}
//If already disabled.
else if (enable == NO && _enable == NO)
{
_IQShowLog(IQLocalizedString(@"already disabled", nil));
}
//If already enabled.
else if (enable == YES && _enable == YES)
{
_IQShowLog(IQLocalizedString(@"already enabled", nil));
}
}
// Setting keyboard distance from text field.
-(void)setKeyboardDistanceFromTextField:(CGFloat)keyboardDistanceFromTextField
{
//Can't be less than zero. Minimum is zero.
_keyboardDistanceFromTextField = MAX(keyboardDistanceFromTextField, 0);
_IQShowLog([NSString stringWithFormat:@"keyboardDistanceFromTextField: %.2f",_keyboardDistanceFromTextField]);
}
/** Enabling/disable gesture on touching. */
-(void)setShouldResignOnTouchOutside:(BOOL)shouldResignOnTouchOutside
{
_IQShowLog([NSString stringWithFormat:@"shouldResignOnTouchOutside: %@",shouldResignOnTouchOutside?@"Yes":@"No"]);
_shouldResignOnTouchOutside = shouldResignOnTouchOutside;
//Enable/Disable gesture recognizer (Enhancement ID: #14)
[_tapGesture setEnabled:_shouldResignOnTouchOutside];
}
/** Enable/disable autotoolbar. Adding and removing toolbar if required. */
-(void)setEnableAutoToolbar:(BOOL)enableAutoToolbar
{
_enableAutoToolbar = enableAutoToolbar;
_IQShowLog([NSString stringWithFormat:@"enableAutoToolbar: %@",enableAutoToolbar?@"Yes":@"No"]);
//If enabled then adding toolbar.
if (_enableAutoToolbar == YES)
{
[self addToolbarIfRequired];
}
//Else removing toolbar.
else
{
[self removeToolbarIfRequired];
}
}
#pragma mark - Private Methods
/** Getting keyWindow. */
-(UIWindow *)keyWindow
{
if (_textFieldView.window)
{
return _textFieldView.window;
}
else
{
static UIWindow *_keyWindow = nil;
/* (Bug ID: #23, #25, #73) */
UIWindow *originalKeyWindow = [[UIApplication sharedApplication] keyWindow];
//If original key window is not nil and the cached keywindow is also not original keywindow then changing keywindow.
if (originalKeyWindow != nil && _keyWindow != originalKeyWindow) _keyWindow = originalKeyWindow;
//Return KeyWindow
return _keyWindow;
}
}
/* Helper function to manipulate RootViewController's frame with animation. */
-(void)setRootViewFrame:(CGRect)frame
{
// Getting topMost ViewController.
UIViewController *controller = [_textFieldView topMostController];
if (controller == nil) controller = [[self keyWindow] topMostController];
//frame size needs to be adjusted on iOS8 due to orientation API changes.
if (IQ_IS_IOS8_OR_GREATER)
{
frame.size = controller.view.frame.size;
}
// If can't get rootViewController then printing warning to user.
if (controller == nil)
_IQShowLog(IQLocalizedString(@"You must set UIWindow.rootViewController in your AppDelegate to work with IQKeyboardManager", nil));
//Used UIViewAnimationOptionBeginFromCurrentState to minimize strange animations.
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
// Setting it's new frame
[controller.view setFrame:frame];
//Animating content (Bug ID: #160)
[controller.view setNeedsLayout];
[controller.view layoutIfNeeded];
_IQShowLog([NSString stringWithFormat:@"Set %@ frame to : %@",[controller _IQDescription],NSStringFromCGRect(frame)]);
} completion:NULL];
}
/* Adjusting RootViewController's frame according to device orientation. */
-(void)adjustFrame
{
// We are unable to get textField object while keyboard showing on UIWebView's textField. (Bug ID: #11)
if (_textFieldView == nil) return;
_IQShowLog([NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]);
// Boolean to know keyboard is showing/hiding
_keyboardManagerFlags.isKeyboardShowing = YES;
// Getting KeyWindow object.
UIWindow *keyWindow = [self keyWindow];
// Getting RootViewController. (Bug ID: #1, #4)
UIViewController *rootController = [_textFieldView topMostController];
if (rootController == nil) rootController = [keyWindow topMostController];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
//If it's iOS8 then we should do calculations according to portrait orientations. // (Bug ID: #64, #66)
UIInterfaceOrientation interfaceOrientation = IQ_IS_IOS8_OR_GREATER ? UIInterfaceOrientationPortrait : [rootController interfaceOrientation];
#pragma GCC diagnostic pop
// Converting Rectangle according to window bounds.
CGRect textFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];
// Getting RootViewRect.
CGRect rootViewRect = [[rootController view] frame];
//Getting statusBarFrame
CGFloat topLayoutGuide = 0;
CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
case UIInterfaceOrientationLandscapeRight:
topLayoutGuide = CGRectGetWidth(statusBarFrame);
break;
case UIInterfaceOrientationPortrait:
case UIInterfaceOrientationPortraitUpsideDown:
topLayoutGuide = CGRectGetHeight(statusBarFrame);
break;
default:
break;
}
CGFloat move = 0;
// Move positive = textField is hidden.
// Move negative = textField is showing.
// Calculating move position. Common for both normal and special cases.
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
move = MIN(CGRectGetMinX(textFieldViewRect)-(topLayoutGuide+5), CGRectGetMaxX(textFieldViewRect)-(CGRectGetWidth(keyWindow.frame)-_kbSize.width));
break;
case UIInterfaceOrientationLandscapeRight:
move = MIN(CGRectGetWidth(keyWindow.frame)-CGRectGetMaxX(textFieldViewRect)-(topLayoutGuide+5), _kbSize.width-CGRectGetMinX(textFieldViewRect));
break;
case UIInterfaceOrientationPortrait:
move = MIN(CGRectGetMinY(textFieldViewRect)-(topLayoutGuide+5), CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-_kbSize.height));
break;
case UIInterfaceOrientationPortraitUpsideDown:
move = MIN(CGRectGetHeight(keyWindow.frame)-CGRectGetMaxY(textFieldViewRect)-(topLayoutGuide+5), _kbSize.height-CGRectGetMinY(textFieldViewRect));
break;
default:
break;
}
_IQShowLog([NSString stringWithFormat:@"Need to move: %.2f",move]);
// Getting it's superScrollView. // (Enhancement ID: #21, #24)
UIScrollView *superScrollView = (UIScrollView*)[_textFieldView superviewOfClassType:[UIScrollView class]];
//If there was a lastScrollView. // (Bug ID: #34)
if (_lastScrollView)
{
//If we can't find current superScrollView, then setting lastScrollView to it's original form.
if (superScrollView == nil)
{
_IQShowLog([NSString stringWithFormat:@"Restoring %@ contentInset to : %@ and contentOffset to : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]);
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
[_lastScrollView setContentInset:_startingContentInsets];
} completion:NULL];
if (_shouldRestoreScrollViewContentOffset)
{
[_lastScrollView setContentOffset:_startingContentOffset animated:YES];
}
_startingContentInsets = UIEdgeInsetsZero;
_startingContentOffset = CGPointZero;
_lastScrollView = nil;
}
//If both scrollView's are different, then reset lastScrollView to it's original frame and setting current scrollView as last scrollView.
else if (superScrollView != _lastScrollView)
{
_IQShowLog([NSString stringWithFormat:@"Restoring %@ contentInset to : %@ and contentOffset to : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]);
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
[_lastScrollView setContentInset:_startingContentInsets];
} completion:NULL];
if (_shouldRestoreScrollViewContentOffset)
{
[_lastScrollView setContentOffset:_startingContentOffset animated:YES];
}
_lastScrollView = superScrollView;
_startingContentInsets = superScrollView.contentInset;
_startingContentOffset = superScrollView.contentOffset;
_IQShowLog([NSString stringWithFormat:@"Saving New %@ contentInset: %@ and contentOffset : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]);
}
//Else the case where superScrollView == lastScrollView means we are on same scrollView after switching to different textField. So doing nothing
}
//If there was no lastScrollView and we found a current scrollView. then setting it as lastScrollView.
else if(superScrollView)
{
_lastScrollView = superScrollView;
_startingContentInsets = superScrollView.contentInset;
_startingContentOffset = superScrollView.contentOffset;
_IQShowLog([NSString stringWithFormat:@"Saving %@ contentInset: %@ and contentOffset : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]);
}
// Special case for ScrollView.
{
// If we found lastScrollView then setting it's contentOffset to show textField.
if (_lastScrollView)
{
//Saving
UIView *lastView = _textFieldView;
UIScrollView *superScrollView = _lastScrollView;
//Looping in upper hierarchy until we don't found any scrollView in it's upper hirarchy till UIWindow object.
while (superScrollView && (move>0?(move > (-superScrollView.contentOffset.y-superScrollView.contentInset.top)):superScrollView.contentOffset.y>0) )
{
//Getting lastViewRect.
CGRect lastViewRect = [[lastView superview] convertRect:lastView.frame toView:superScrollView];
//Calculating the expected Y offset from move and scrollView's contentOffset.
CGFloat shouldOffsetY = superScrollView.contentOffset.y - MIN(superScrollView.contentOffset.y,-move);
//Rearranging the expected Y offset according to the view.
shouldOffsetY = MIN(shouldOffsetY, lastViewRect.origin.y/*-5*/); //-5 is for good UI.//Commenting -5 (Bug ID: #69)
//[superScrollView superviewOfClassType:[UIScrollView class]] == nil If processing scrollView is last scrollView in upper hierarchy (there is no other scrollView upper hierrchy.)
//[_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type
//shouldOffsetY > 0 shouldOffsetY must be greater than in order to keep distance from navigationBar (Bug ID: #92)
if ([_textFieldView isKindOfClass:[UITextView class]] && [superScrollView superviewOfClassType:[UIScrollView class]] == nil && shouldOffsetY > 0)
{
CGFloat maintainTopLayout = 0;
//When uncommenting this, each calculation goes to well, but don't know why scrollView doesn't adjusting it's contentOffset at bottom
// if ([_textFieldView.viewController respondsToSelector:@selector(topLayoutGuide)])
// maintainTopLayout = [_textFieldView.viewController.topLayoutGuide length];
// else
maintainTopLayout = CGRectGetMaxY(_textFieldView.viewController.navigationController.navigationBar.frame);
maintainTopLayout+= 10; //For good UI
// Converting Rectangle according to window bounds.
CGRect currentTextFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];
CGFloat expectedFixDistance = shouldOffsetY;
//Calculating expected fix distance which needs to be managed from navigation bar
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
expectedFixDistance = CGRectGetMinX(currentTextFieldViewRect) - maintainTopLayout;
break;
case UIInterfaceOrientationLandscapeRight:
expectedFixDistance = (CGRectGetWidth(keyWindow.frame)-CGRectGetMaxX(currentTextFieldViewRect)) - maintainTopLayout;
break;
case UIInterfaceOrientationPortrait:
expectedFixDistance = CGRectGetMinY(currentTextFieldViewRect) - maintainTopLayout;
break;
case UIInterfaceOrientationPortraitUpsideDown:
expectedFixDistance = (CGRectGetHeight(keyWindow.frame)-CGRectGetMaxY(currentTextFieldViewRect)) - maintainTopLayout;
break;
default:
break;
}
//Now if expectedOffsetY (superScrollView.contentOffset.y + expectedFixDistance) is lower than current shouldOffsetY, which means we're in a position where navigationBar up and hide, then reducing shouldOffsetY with expectedOffsetY (superScrollView.contentOffset.y + expectedFixDistance)
shouldOffsetY = MIN(shouldOffsetY, superScrollView.contentOffset.y + expectedFixDistance);
//Setting move to 0 because now we don't want to move any view anymore (All will be managed by our contentInset logic.
move = 0;
}
else
{
//Subtracting the Y offset from the move variable, because we are going to change scrollView's contentOffset.y to shouldOffsetY.
move -= (shouldOffsetY-superScrollView.contentOffset.y);
}
//Getting problem while using `setContentOffset:animated:`, So I used animation API.
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
_IQShowLog([NSString stringWithFormat:@"Adjusting %.2f to %@ ContentOffset",(superScrollView.contentOffset.y-shouldOffsetY),[superScrollView _IQDescription]]);
_IQShowLog([NSString stringWithFormat:@"Remaining Move: %.2f",move]);
superScrollView.contentOffset = CGPointMake(superScrollView.contentOffset.x, shouldOffsetY);
} completion:NULL];
// Getting next lastView & superScrollView.
lastView = superScrollView;
superScrollView = (UIScrollView*)[lastView superviewOfClassType:[UIScrollView class]];
}
//Updating contentInset
{
CGFloat bottom = 0;
CGRect lastScrollViewRect = [[_lastScrollView superview] convertRect:_lastScrollView.frame toView:keyWindow];
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
bottom = _kbSize.width-(CGRectGetWidth(keyWindow.frame)-CGRectGetMaxX(lastScrollViewRect));
break;
case UIInterfaceOrientationLandscapeRight:
bottom = _kbSize.width-CGRectGetMinX(lastScrollViewRect);
break;
case UIInterfaceOrientationPortrait:
bottom = _kbSize.height-(CGRectGetHeight(keyWindow.frame)-CGRectGetMaxY(lastScrollViewRect));
break;
case UIInterfaceOrientationPortraitUpsideDown:
bottom = _kbSize.height-CGRectGetMinY(lastScrollViewRect);
break;
default:
break;
}
// Update the insets so that the scroll vew doesn't shift incorrectly when the offset is near the bottom of the scroll view.
UIEdgeInsets movedInsets = _lastScrollView.contentInset;
movedInsets.bottom = MAX(_startingContentInsets.bottom, bottom);
_IQShowLog([NSString stringWithFormat:@"%@ old ContentInset : %@",[_lastScrollView _IQDescription], NSStringFromUIEdgeInsets(_lastScrollView.contentInset)]);
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
_lastScrollView.contentInset = movedInsets;
} completion:NULL];
if (_lastScrollView.contentSize.height<_lastScrollView.frame.size.height)
{
CGSize contentSize = _lastScrollView.contentSize;
contentSize.height = _lastScrollView.frame.size.height;
_lastScrollView.contentSize = contentSize;
}
_IQShowLog([NSString stringWithFormat:@"%@ new ContentInset : %@",[_lastScrollView _IQDescription], NSStringFromUIEdgeInsets(_lastScrollView.contentInset)]);
}
}
//Going ahead. No else if.
}
//Special case for UITextView(Readjusting the move variable when textView hight is too big to fit on screen)
//_canAdjustTextView If we have permission to adjust the textView, then let's do it on behalf of user (Enhancement ID: #15)
//_lastScrollView If not having inside any scrollView, (now contentInset manages the full screen textView.
//[_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type
//_isTextFieldViewFrameChanged If frame is not change by library in past (Bug ID: #92)
if (_canAdjustTextView && (_lastScrollView == NO) && [_textFieldView isKindOfClass:[UITextView class]] && _keyboardManagerFlags.isTextFieldViewFrameChanged == NO)
{
CGFloat textViewHeight = CGRectGetHeight(_textFieldView.frame);
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
case UIInterfaceOrientationLandscapeRight:
textViewHeight = MIN(textViewHeight, (CGRectGetWidth(keyWindow.frame)-_kbSize.width-(topLayoutGuide+5)));
break;
case UIInterfaceOrientationPortrait:
case UIInterfaceOrientationPortraitUpsideDown:
textViewHeight = MIN(textViewHeight, (CGRectGetHeight(keyWindow.frame)-_kbSize.height-(topLayoutGuide+5)));
break;
default:
break;
}
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
_IQShowLog([NSString stringWithFormat:@"%@ Old Frame : %@",[_textFieldView _IQDescription], NSStringFromCGRect(_textFieldView.frame)]);
CGRect textFieldViewRect = _textFieldView.frame;
textFieldViewRect.size.height = textViewHeight;
_textFieldView.frame = textFieldViewRect;
_keyboardManagerFlags.isTextFieldViewFrameChanged = YES;
_IQShowLog([NSString stringWithFormat:@"%@ New Frame : %@",[_textFieldView _IQDescription], NSStringFromCGRect(_textFieldView.frame)]);
} completion:NULL];
}
// Special case for iPad modalPresentationStyle.
if ([rootController modalPresentationStyle] == UIModalPresentationFormSheet ||
[rootController modalPresentationStyle] == UIModalPresentationPageSheet)
{
_IQShowLog([NSString stringWithFormat:@"Found Special case for Model Presentation Style: %ld",(long)(rootController.modalPresentationStyle)]);
// Positive or zero.
if (move>=0)
{
// We should only manipulate y.
rootViewRect.origin.y -= move;
// From now prevent keyboard manager to slide up the rootView to more than keyboard height. (Bug ID: #93)
if (_preventShowingBottomBlankSpace == YES)
{
CGFloat minimumY = 0;
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
case UIInterfaceOrientationLandscapeRight:
minimumY = CGRectGetWidth(keyWindow.frame)-rootViewRect.size.height-topLayoutGuide-(_kbSize.width-_keyboardDistanceFromTextField); break;
case UIInterfaceOrientationPortrait:
case UIInterfaceOrientationPortraitUpsideDown:
minimumY = (CGRectGetHeight(keyWindow.frame)-rootViewRect.size.height-topLayoutGuide)/2-(_kbSize.height-_keyboardDistanceFromTextField); break;
default: break;
}
rootViewRect.origin.y = MAX(rootViewRect.origin.y, minimumY);
}
_IQShowLog(@"Moving Upward");
// Setting adjusted rootViewRect
[self setRootViewFrame:rootViewRect];
}
// Negative
else
{
// Calculating disturbed distance. Pull Request #3
CGFloat disturbDistance = CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect);
// disturbDistance Negative = frame disturbed.
// disturbDistance positive = frame not disturbed.
if(disturbDistance<0)
{
// We should only manipulate y.
rootViewRect.origin.y -= MAX(move, disturbDistance);
_IQShowLog(@"Moving Downward");
// Setting adjusted rootViewRect
[self setRootViewFrame:rootViewRect];
}
}
}
//If presentation style is neither UIModalPresentationFormSheet nor UIModalPresentationPageSheet then going ahead.(General case)
else
{
// Positive or zero.
if (move>=0)
{
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft: rootViewRect.origin.x -= move; break;
case UIInterfaceOrientationLandscapeRight: rootViewRect.origin.x += move; break;
case UIInterfaceOrientationPortrait: rootViewRect.origin.y -= move; break;
case UIInterfaceOrientationPortraitUpsideDown: rootViewRect.origin.y += move; break;
default: break;
}
// From now prevent keyboard manager to slide up the rootView to more than keyboard height. (Bug ID: #93)
if (_preventShowingBottomBlankSpace == YES)
{
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft: rootViewRect.origin.x = MAX(rootViewRect.origin.x, MIN(0,-_kbSize.width+_keyboardDistanceFromTextField)); break;
case UIInterfaceOrientationLandscapeRight: rootViewRect.origin.x = MIN(rootViewRect.origin.x, +_kbSize.width-_keyboardDistanceFromTextField); break;
case UIInterfaceOrientationPortrait: rootViewRect.origin.y = MAX(rootViewRect.origin.y, MIN(0, -_kbSize.height+_keyboardDistanceFromTextField)); break;
case UIInterfaceOrientationPortraitUpsideDown: rootViewRect.origin.y = MIN(rootViewRect.origin.y, +_kbSize.height-_keyboardDistanceFromTextField); break;
default: break;
}
}
_IQShowLog(@"Moving Upward");
// Setting adjusted rootViewRect
[self setRootViewFrame:rootViewRect];
}
// Negative
else
{
CGFloat disturbDistance = 0;
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
disturbDistance = CGRectGetMinX(rootViewRect)-CGRectGetMinX(_topViewBeginRect);
break;
case UIInterfaceOrientationLandscapeRight:
disturbDistance = CGRectGetMinX(_topViewBeginRect)-CGRectGetMinX(rootViewRect);
break;
case UIInterfaceOrientationPortrait:
disturbDistance = CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect);
break;
case UIInterfaceOrientationPortraitUpsideDown:
disturbDistance = CGRectGetMinY(_topViewBeginRect)-CGRectGetMinY(rootViewRect);
break;
default:
break;
}
// disturbDistance Negative = frame disturbed. Pull Request #3
// disturbDistance positive = frame not disturbed.
if(disturbDistance<0)
{
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft: rootViewRect.origin.x -= MAX(move, disturbDistance); break;
case UIInterfaceOrientationLandscapeRight: rootViewRect.origin.x += MAX(move, disturbDistance); break;
case UIInterfaceOrientationPortrait: rootViewRect.origin.y -= MAX(move, disturbDistance); break;
case UIInterfaceOrientationPortraitUpsideDown: rootViewRect.origin.y += MAX(move, disturbDistance); break;
default: break;
}
_IQShowLog(@"Moving Downward");
// Setting adjusted rootViewRect
[self setRootViewFrame:rootViewRect];
}
}
}
_IQShowLog([NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector(_cmd)]);
}
#pragma mark - UIKeyboad Notification methods
/* UIKeyboardWillShowNotification. */
-(void)keyboardWillShow:(NSNotification*)aNotification
{
_kbShowNotification = aNotification;
if (_enable == NO) return;
_IQShowLog([NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]);
//Due to orientation callback we need to resave it's original frame. // (Bug ID: #46)
//Added _isTextFieldViewFrameChanged check. Saving textFieldView current frame to use it with canAdjustTextView if textViewFrame has already not been changed. (Bug ID: #92)
if (_keyboardManagerFlags.isTextFieldViewFrameChanged == NO && _textFieldView)
{
_textFieldViewIntialFrame = _textFieldView.frame;
_IQShowLog([NSString stringWithFormat:@"Saving %@ Initial frame :%@",[_textFieldView _IQDescription],NSStringFromCGRect(_textFieldViewIntialFrame)]);
}
if (CGRectEqualToRect(_topViewBeginRect, CGRectZero)) // (Bug ID: #5)
{
// keyboard is not showing(At the beginning only). We should save rootViewRect.
_rootViewController = [_textFieldView topMostController];
if (_rootViewController == nil) _rootViewController = [[self keyWindow] topMostController];
_topViewBeginRect = _rootViewController.view.frame;
_IQShowLog([NSString stringWithFormat:@"Saving %@ beginning Frame: %@",[_rootViewController _IQDescription] ,NSStringFromCGRect(_topViewBeginRect)]);
}
if (_shouldAdoptDefaultKeyboardAnimation)
{
// Getting keyboard animation.
_animationCurve = [[[aNotification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
_animationCurve = _animationCurve<<16;
}
else
{
_animationCurve = 0;
}
// Getting keyboard animation duration
CGFloat duration = [[[aNotification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue];
//Saving animation duration
if (duration != 0.0) _animationDuration = duration;
CGSize oldKBSize = _kbSize;
// Getting UIKeyboardSize.
// CGRect screenRect = [self keyWindow].bounds;
CGRect kbFrame = [[[aNotification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
_kbSize = kbFrame.size;
_IQShowLog([NSString stringWithFormat:@"UIKeyboard Size : %@",NSStringFromCGSize(_kbSize)]);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
//If it's iOS8 then we should do calculations according to portrait orientations. // (Bug ID: #64, #66)
UIViewController *topMostController = [_textFieldView topMostController];
if (topMostController == nil) topMostController = [[self keyWindow] topMostController];
UIInterfaceOrientation interfaceOrientation = IQ_IS_IOS8_OR_GREATER ? UIInterfaceOrientationPortrait : [topMostController interfaceOrientation];
#pragma GCC diagnostic pop
switch (interfaceOrientation)
{
case UIInterfaceOrientationLandscapeLeft:
// _kbSize.width = screenRect.size.width - kbFrame.origin.x;
_kbSize.width += _keyboardDistanceFromTextField;
break;
case UIInterfaceOrientationLandscapeRight:
// _kbSize.width = screenRect.size.width - kbFrame.origin.x;
_kbSize.width += _keyboardDistanceFromTextField;
break;
case UIInterfaceOrientationPortrait:
// _kbSize.height = screenRect.size.height - kbFrame.origin.y;
_kbSize.height += _keyboardDistanceFromTextField;
break;
case UIInterfaceOrientationPortraitUpsideDown:
// _kbSize.height = screenRect.size.height - kbFrame.origin.y;
_kbSize.height += _keyboardDistanceFromTextField;
break;
default:
break;
}
//If last restored keyboard size is different(any orientation accure), then refresh. otherwise not.
if (!CGSizeEqualToSize(_kbSize, oldKBSize))
{
//If _textFieldView is inside ignored responder then do nothing. (Bug ID: #37, #74, #76)
//See notes:- https://developer.apple.com/Library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html. If it is UIAlertView textField then do not affect anything (Bug ID: #70).
if (_textFieldView != nil && [_textFieldView isAlertViewTextField] == NO)
{
UIViewController *textFieldViewController = [_textFieldView viewController];
BOOL shouldIgnore = NO;
for (Class disabledClass in _disabledClasses)
{
if ([textFieldViewController isKindOfClass:disabledClass])
{
shouldIgnore = YES;
break;
}
}
if (shouldIgnore == NO)
{
[self adjustFrame];
}
}
}
_IQShowLog([NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector(_cmd)]);
}
/* UIKeyboardWillHideNotification. So setting rootViewController to it's default frame. */
- (void)keyboardWillHide:(NSNotification*)aNotification
{
//If it's not a fake notification generated by [self setEnable:NO].
if (aNotification != nil) _kbShowNotification = nil;
//If not enabled then do nothing.
if (_enable == NO) return;
_IQShowLog([NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]);
//Commented due to #56. Added all the conditions below to handle UIWebView's textFields. (Bug ID: #56)
// We are unable to get textField object while keyboard showing on UIWebView's textField. (Bug ID: #11)
// if (_textFieldView == nil) return;
// Boolean to know keyboard is showing/hiding
_keyboardManagerFlags.isKeyboardShowing = NO;
// Getting keyboard animation duration
CGFloat aDuration = [[[aNotification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue];
if (aDuration!= 0.0f)
{
// Setitng keyboard animation duration
_animationDuration = aDuration;
}
//Restoring the contentOffset of the lastScrollView
if (_lastScrollView)
{
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
_lastScrollView.contentInset = _startingContentInsets;
if (_shouldRestoreScrollViewContentOffset)
{
_lastScrollView.contentOffset = _startingContentOffset;
}
_IQShowLog([NSString stringWithFormat:@"Restoring %@ contentInset to : %@ and contentOffset to : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]);
// TODO: restore scrollView state
// This is temporary solution. Have to implement the save and restore scrollView state
UIScrollView *superscrollView = _lastScrollView;
while ((superscrollView = (UIScrollView*)[superscrollView superviewOfClassType:[UIScrollView class]]))
{
MAX(superscrollView.contentSize.height, CGRectGetHeight(superscrollView.frame));
CGSize contentSize = CGSizeMake(MAX(superscrollView.contentSize.width, CGRectGetWidth(superscrollView.frame)), MAX(superscrollView.contentSize.height, CGRectGetHeight(superscrollView.frame)));
CGFloat minimumY = contentSize.height-CGRectGetHeight(superscrollView.frame);
if (minimumY<superscrollView.contentOffset.y)
{
superscrollView.contentOffset = CGPointMake(superscrollView.contentOffset.x, minimumY);
_IQShowLog([NSString stringWithFormat:@"Restoring %@ contentOffset to : %@",[superscrollView _IQDescription],NSStringFromCGPoint(superscrollView.contentOffset)]);
}
}
} completion:NULL];
}
// Setting rootViewController frame to it's original position. // (Bug ID: #18)
if (!CGRectEqualToRect(_topViewBeginRect, CGRectZero) && _rootViewController)
{
//frame size needs to be adjusted on iOS8 due to orientation API changes.
if (IQ_IS_IOS8_OR_GREATER)
{
_topViewBeginRect.size = _rootViewController.view.frame.size;
}
//Used UIViewAnimationOptionBeginFromCurrentState to minimize strange animations.
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
_IQShowLog([NSString stringWithFormat:@"Restoring %@ frame to : %@",[_rootViewController _IQDescription],NSStringFromCGRect(_topViewBeginRect)]);
// Setting it's new frame
[_rootViewController.view setFrame:_topViewBeginRect];
//Animating content (Bug ID: #160)
[_rootViewController.view setNeedsLayout];
[_rootViewController.view layoutIfNeeded];
} completion:NULL];
_rootViewController = nil;
}
//Reset all values
_lastScrollView = nil;
_kbSize = CGSizeZero;
_startingContentInsets = UIEdgeInsetsZero;
_startingContentOffset = CGPointZero;
// topViewBeginRect = CGRectZero; //Commented due to #82
_IQShowLog([NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector(_cmd)]);
}
/* UIKeyboardDidHideNotification. So topViewBeginRect can be set to CGRectZero. */
- (void)keyboardDidHide:(NSNotification*)aNotification
{
_IQShowLog([NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]);
_topViewBeginRect = CGRectZero;
_IQShowLog([NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector(_cmd)]);
}
#pragma mark - UITextFieldView Delegate methods
/** UITextFieldTextDidBeginEditingNotification, UITextViewTextDidBeginEditingNotification. Fetching UITextFieldView object. */
-(void)textFieldViewDidBeginEditing:(NSNotification*)notification
{
_IQShowLog([NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]);
// Getting object
_textFieldView = notification.object;
if (_overrideKeyboardAppearance == YES)
{
UITextField *textField = (UITextField*)_textFieldView;
//If keyboard appearance is not like the provided appearance
if (textField.keyboardAppearance != _keyboardAppearance)
{
//Setting textField keyboard appearance and reloading inputViews.
textField.keyboardAppearance = _keyboardAppearance;
[textField reloadInputViews];
}
}
// Saving textFieldView current frame to use it with canAdjustTextView if textViewFrame has already not been changed.
//Added _isTextFieldViewFrameChanged check. (Bug ID: #92)
if (_keyboardManagerFlags.isTextFieldViewFrameChanged == NO && _textFieldView)
{
_textFieldViewIntialFrame = _textFieldView.frame;
_IQShowLog([NSString stringWithFormat:@"Saving %@ Initial frame :%@",[_textFieldView _IQDescription],NSStringFromCGRect(_textFieldViewIntialFrame)]);
}
//If autoToolbar enable, then add toolbar on all the UITextField/UITextView's if required.
if (_enableAutoToolbar)
{
_IQShowLog(@"adding UIToolbars if required");
//UITextView special case. Keyboard Notification is firing before textView notification so we need to reload it's inputViews.
if ([_textFieldView isKindOfClass:[UITextView class]] && _textFieldView.inputAccessoryView == nil)
{
[UIView animateWithDuration:0.00001 delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
[self addToolbarIfRequired];
} completion:^(BOOL finished) {
//On textView toolbar didn't appear on first time, so forcing textView to reload it's inputViews.
[_textFieldView reloadInputViews];
}];
}
else
{
//Adding toolbar
[self addToolbarIfRequired];
}
}
if (_enable == NO)
{
_IQShowLog([NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector(_cmd)]);
return;
}
//Adding Geture recognizer to window (Enhancement ID: #14)
[_textFieldView.window addGestureRecognizer:_tapGesture];
if (_keyboardManagerFlags.isKeyboardShowing == NO) // (Bug ID: #5)
{
// keyboard is not showing(At the beginning only). We should save rootViewRect.
_rootViewController = [_textFieldView topMostController];
if (_rootViewController == nil) _rootViewController = [[self keyWindow] topMostController];
_topViewBeginRect = _rootViewController.view.frame;
_IQShowLog([NSString stringWithFormat:@"Saving %@ beginning Frame: %@",[_rootViewController _IQDescription], NSStringFromCGRect(_topViewBeginRect)]);
}
//If _textFieldView is inside ignored responder then do nothing. (Bug ID: #37, #74, #76)
//See notes:- https://developer.apple.com/Library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html. If it is UIAlertView textField then do not affect anything (Bug ID: #70).
if (_textFieldView != nil && [_textFieldView isAlertViewTextField] == NO)
{
//Getting textField viewController
UIViewController *textFieldViewController = [_textFieldView viewController];
BOOL shouldIgnore = NO;
for (Class disabledClass in _disabledClasses)
{
//If viewController is kind of disabled viewController class, then ignoring to adjust view.
if ([textFieldViewController isKindOfClass:disabledClass])
{
shouldIgnore = YES;
break;
}
}
//If shouldn't ignore.
if (shouldIgnore == NO)
{
// keyboard is already showing. adjust frame.
[self adjustFrame];
}
}
_IQShowLog([NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector(_cmd)]);
}
/** UITextFieldTextDidEndEditingNotification, UITextViewTextDidEndEditingNotification. Removing fetched object. */
-(void)textFieldViewDidEndEditing:(NSNotification*)notification
{
_IQShowLog([NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]);
//Removing gesture recognizer (Enhancement ID: #14)
[_textFieldView.window removeGestureRecognizer:_tapGesture];
// We check if there's a change in original frame or not.
if(_keyboardManagerFlags.isTextFieldViewFrameChanged == YES)
{
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
_keyboardManagerFlags.isTextFieldViewFrameChanged = NO;
_IQShowLog([NSString stringWithFormat:@"Restoring %@ frame to : %@",[_textFieldView _IQDescription],NSStringFromCGRect(_textFieldViewIntialFrame)]);
//Setting textField to it's initial frame
_textFieldView.frame = _textFieldViewIntialFrame;
} completion:NULL];
}
//Setting object to nil
_textFieldView = nil;
_IQShowLog([NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector(_cmd)]);
}
/* UITextViewTextDidChangeNotificationBug, fix for iOS 7.0.x - http://stackoverflow.com/questions/18966675/uitextview-in-ios7-clips-the-last-line-of-text-string */
-(void)textFieldViewDidChange:(NSNotification*)notification // (Bug ID: #18)
{
if (_shouldFixTextViewClip == YES)
{
UITextView *textView = (UITextView *)notification.object;
CGRect line = [textView caretRectForPosition: textView.selectedTextRange.start];
CGFloat overflow = CGRectGetMaxY(line) - (textView.contentOffset.y + CGRectGetHeight(textView.bounds) - textView.contentInset.bottom - textView.contentInset.top);
//Added overflow conditions (Bug ID: 95)
if ( overflow > 0 && overflow < FLT_MAX)
{
// We are at the bottom of the visible text and introduced a line feed, scroll down (iOS 7 does not do it)
// Scroll caret to visible area
CGPoint offset = textView.contentOffset;
offset.y += overflow + 7; // leave 7 pixels margin
// Cannot animate with setContentOffset:animated: or caret will not appear
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
[textView setContentOffset:offset];
} completion:NULL];
}
}
}
#pragma mark - UIInterfaceOrientation Change notification methods
/** UIApplicationWillChangeStatusBarOrientationNotification. Need to set the textView to it's original position. If any frame changes made. (Bug ID: #92)*/
- (void)willChangeStatusBarOrientation:(NSNotification*)aNotification
{
_IQShowLog([NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]);
//If textFieldViewInitialRect is saved then restore it.(UITextView case @canAdjustTextView)
if (_keyboardManagerFlags.isTextFieldViewFrameChanged == YES)
{
//Due to orientation callback we need to set it's original position.
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
_keyboardManagerFlags.isTextFieldViewFrameChanged = NO;
_IQShowLog([NSString stringWithFormat:@"Restoring %@ frame to : %@",[_textFieldView _IQDescription],NSStringFromCGRect(_textFieldViewIntialFrame)]);
//Setting textField to it's initial frame
_textFieldView.frame = _textFieldViewIntialFrame;
} completion:NULL];
}
_IQShowLog([NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector(_cmd)]);
}
#pragma mark AutoResign methods
/** Resigning on tap gesture. */
- (void)tapRecognized:(UITapGestureRecognizer*)gesture // (Enhancement ID: #14)
{
if (gesture.state == UIGestureRecognizerStateEnded)
{
//Resigning currently responder textField.
[self resignFirstResponder];
}
}
/** Note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES. */
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return NO;
}
/** To not detect touch events in a subclass of UIControl, these may have added their own selector for specific work */
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
// Should not recognize gesture if the clicked view is either UIControl or UINavigationBar(<Back button etc...) (Bug ID: #145)
return ([[touch view] isKindOfClass:[UIControl class]] || [[touch view] isKindOfClass:[UINavigationBar class]]) ? NO : YES;
}
/** Resigning textField. */
- (void)resignFirstResponder
{
if (_textFieldView)
{
// Retaining textFieldView
UIView *textFieldRetain = _textFieldView;
//Resigning first responder
BOOL isResignFirstResponder = [_textFieldView resignFirstResponder];
// If it refuses then becoming it as first responder again. (Bug ID: #96)
if (isResignFirstResponder == NO)
{
//If it refuses to resign then becoming it first responder again for getting notifications callback.
[textFieldRetain becomeFirstResponder];
_IQShowLog([NSString stringWithFormat:@"Refuses to Resign first responder: %@",[_textFieldView _IQDescription]]);
}
else if (textFieldRetain.doneInvocation)
{
[textFieldRetain.doneInvocation invoke];
}
}
}
/** Returns YES if can navigate to previous responder textField/textView, otherwise NO. */
-(BOOL)canGoPrevious
{
//Getting all responder view's.
NSArray *textFields = [self responderViews];
if ([textFields containsObject:_textFieldView])
{
//Getting index of current textField.
NSUInteger index = [textFields indexOfObject:_textFieldView];
//If it is not first textField. then it's previous object can becomeFirstResponder.
if (index > 0)
{
return YES;
}
}
return NO;
}
/** Returns YES if can navigate to next responder textField/textView, otherwise NO. */
-(BOOL)canGoNext
{
//Getting all responder view's.
NSArray *textFields = [self responderViews];
if ([textFields containsObject:_textFieldView])
{
//Getting index of current textField.
NSUInteger index = [textFields indexOfObject:_textFieldView];
//If it is not last textField. then it's next object becomeFirstResponder.
if (index < textFields.count-1)
{
return YES;
}
}
return NO;
}
/** Navigate to previous responder textField/textView. */
-(void)goPrevious
{
//Getting all responder view's.
NSArray *textFields = [self responderViews];
if ([textFields containsObject:_textFieldView])
{
//Getting index of current textField.
NSUInteger index = [textFields indexOfObject:_textFieldView];
//If it is not first textField. then it's previous object becomeFirstResponder.
if (index > 0)
{
UITextField *nextTextField = [textFields objectAtIndex:index-1];
// Retaining textFieldView
UIView *textFieldRetain = _textFieldView;
BOOL isAcceptAsFirstResponder = [nextTextField becomeFirstResponder];
// If it refuses then becoming previous textFieldView as first responder again. (Bug ID: #96)
if (isAcceptAsFirstResponder == NO)
{
//If next field refuses to become first responder then restoring old textField as first responder.
[textFieldRetain becomeFirstResponder];
_IQShowLog([NSString stringWithFormat:@"Refuses to become first responder: %@",[nextTextField _IQDescription]]);
}
else if (textFieldRetain.previousInvocation)
{
[textFieldRetain.previousInvocation invoke];
}
}
}
}
/** Navigate to next responder textField/textView. */
-(void)goNext
{
//Getting all responder view's.
NSArray *textFields = [self responderViews];
if ([textFields containsObject:_textFieldView])
{
//Getting index of current textField.
NSUInteger index = [textFields indexOfObject:_textFieldView];
//If it is not last textField. then it's next object becomeFirstResponder.
if (index < textFields.count-1)
{
UITextField *nextTextField = [textFields objectAtIndex:index+1];
// Retaining textFieldView
UIView *textFieldRetain = _textFieldView;
BOOL isAcceptAsFirstResponder = [nextTextField becomeFirstResponder];
// If it refuses then becoming previous textFieldView as first responder again. (Bug ID: #96)
if (isAcceptAsFirstResponder == NO)
{
//If next field refuses to become first responder then restoring old textField as first responder.
[textFieldRetain becomeFirstResponder];
_IQShowLog([NSString stringWithFormat:@"Refuses to become first responder: %@",[nextTextField _IQDescription]]);
}
else if (textFieldRetain.nextInvocation)
{
[textFieldRetain.nextInvocation invoke];
}
}
}
}
#pragma mark AutoToolbar methods
/** Get all UITextField/UITextView siblings of textFieldView. */
-(NSArray*)responderViews
{
UIView *superConsideredView;
//If find any consider responderView in it's upper hierarchy then will get deepResponderView.
for (Class consideredClass in _toolbarPreviousNextConsideredClass)
{
superConsideredView = [_textFieldView superviewOfClassType:consideredClass];
if (superConsideredView != nil)
break;
}
//If there is a superConsideredView in view's hierarchy, then fetching all it's subview that responds. No sorting for superConsideredView, it's by subView position. (Enhancement ID: #22)
if (superConsideredView)
{
return [superConsideredView deepResponderViews];
}
//Otherwise fetching all the siblings
else
{
NSArray *textFields = [_textFieldView responderSiblings];
//Sorting textFields according to behaviour
switch (_toolbarManageBehaviour)
{
//If autoToolbar behaviour is bySubviews, then returning it.
case IQAutoToolbarBySubviews:
return textFields;
break;
//If autoToolbar behaviour is by tag, then sorting it according to tag property.
case IQAutoToolbarByTag:
return [textFields sortedArrayByTag];
break;
//If autoToolbar behaviour is by tag, then sorting it according to tag property.
case IQAutoToolbarByPosition:
return [textFields sortedArrayByPosition];
break;
default:
return nil;
break;
}
}
}
/** Add toolbar if it is required to add on textFields and it's siblings. */
-(void)addToolbarIfRequired
{
UIViewController *textFieldViewController = [_textFieldView viewController];
//If found any toolbar disabled classes then return. Will not add any toolbar.
for (Class disabledToolbarClass in _disabledToolbarClasses)
if ([textFieldViewController isKindOfClass:disabledToolbarClass])
return;
// Getting all the sibling textFields.
NSArray *siblings = [self responderViews];
// If only one object is found, then adding only Done button.
if (siblings.count==1)
{
UITextField *textField = nil;
if ([siblings count])
textField = [siblings objectAtIndex:0];
//Either there is no inputAccessoryView or if accessoryView is not appropriate for current situation(There is Previous/Next/Done toolbar).
if (![textField inputAccessoryView] || ([[textField inputAccessoryView] tag] == kIQPreviousNextButtonToolbarTag))
{
static UIView *doneToolbar = nil;
if (doneToolbar == nil)
{
//Now adding textField placeholder text as title of IQToolbar (Enhancement ID: #27)
[textField addDoneOnKeyboardWithTarget:self action:@selector(doneAction:) shouldShowPlaceholder:_shouldShowTextFieldPlaceholder];
doneToolbar = textField.inputAccessoryView;
doneToolbar.tag = kIQDoneButtonToolbarTag; // (Bug ID: #78)
}
else
{
textField.inputAccessoryView = doneToolbar;
}
}
if ([textField.inputAccessoryView isKindOfClass:[IQToolbar class]] && textField.inputAccessoryView.tag == kIQDoneButtonToolbarTag)
{
IQToolbar *toolbar = (IQToolbar*)[textField inputAccessoryView];
//Bar style according to keyboard appearance
if (IQ_IS_IOS7_OR_GREATER && [textField respondsToSelector:@selector(keyboardAppearance)])
{
switch ([(UITextField*)textField keyboardAppearance])
{
case UIKeyboardAppearanceAlert:
{
toolbar.barStyle = UIBarStyleBlack;
if ([toolbar respondsToSelector:@selector(tintColor)])
[toolbar setTintColor:[UIColor whiteColor]];
}
break;
default:
{
toolbar.barStyle = UIBarStyleDefault;
#ifdef NSFoundationVersionNumber_iOS_6_1
if ([toolbar respondsToSelector:@selector(tintColor)])
[toolbar setTintColor:_shouldToolbarUsesTextFieldTintColor?[textField tintColor]:_defaultToolbarTintColor];
#endif
}
break;
}
}
//If need to show placeholder
if (_shouldShowTextFieldPlaceholder)
{
//Updating placeholder font to toolbar. //(Bug ID: #148)
if ([textField respondsToSelector:@selector(placeholder)] && [toolbar.title isEqualToString:textField.placeholder] == NO)
[toolbar setTitle:textField.placeholder];
//Setting toolbar title font. // (Enhancement ID: #30)
if (_placeholderFont && [_placeholderFont isKindOfClass:[UIFont class]])
[toolbar setTitleFont:_placeholderFont];
}
}
}
else if(siblings.count)
{
// If more than 1 textField is found. then adding previous/next/done buttons on it.
for (UITextField *textField in siblings)
{
//Either there is no inputAccessoryView or if accessoryView is not appropriate for current situation(There is Done toolbar).
if (![textField inputAccessoryView] || [[textField inputAccessoryView] tag] == kIQDoneButtonToolbarTag)
{
//Now adding textField placeholder text as title of IQToolbar (Enhancement ID: #27)
[textField addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:) shouldShowPlaceholder:_shouldShowTextFieldPlaceholder];
textField.inputAccessoryView.tag = kIQPreviousNextButtonToolbarTag; // (Bug ID: #78)
}
if ([textField.inputAccessoryView isKindOfClass:[IQToolbar class]] && textField.inputAccessoryView.tag == kIQPreviousNextButtonToolbarTag)
{
IQToolbar *toolbar = (IQToolbar*)[textField inputAccessoryView];
//Bar style according to keyboard appearance
if (IQ_IS_IOS7_OR_GREATER && [textField respondsToSelector:@selector(keyboardAppearance)])
{
switch ([(UITextField*)textField keyboardAppearance])
{
case UIKeyboardAppearanceAlert:
{
toolbar.barStyle = UIBarStyleBlack;
if ([toolbar respondsToSelector:@selector(tintColor)])
[toolbar setTintColor:[UIColor whiteColor]];
}
break;
default:
{
toolbar.barStyle = UIBarStyleDefault;
#ifdef NSFoundationVersionNumber_iOS_6_1
//Setting toolbar tintColor // (Enhancement ID: #30)
if ([toolbar respondsToSelector:@selector(tintColor)])
[toolbar setTintColor:_shouldToolbarUsesTextFieldTintColor?[textField tintColor]:_defaultToolbarTintColor];
#endif
}
break;
}
}
//In case of UITableView (Special), the next/previous buttons has to be refreshed everytime. (Bug ID: #56)
// If firstTextField, then previous should not be enabled.
if ([siblings objectAtIndex:0] == textField)
{
[textField setEnablePrevious:NO next:YES];
}
// If lastTextField then next should not be enaled.
else if ([siblings lastObject] == textField)
{
[textField setEnablePrevious:YES next:NO];
}
else
{
[textField setEnablePrevious:YES next:YES];
}
}
}
}
}
/** Remove any toolbar if it is IQToolbar. */
-(void)removeToolbarIfRequired // (Bug ID: #18)
{
// Getting all the sibling textFields.
NSArray *siblings = [self responderViews];
for (UITextField *textField in siblings)
{
UIView *toolbar = [textField inputAccessoryView];
// (Bug ID: #78)
if ([toolbar isKindOfClass:[IQToolbar class]] && (toolbar.tag == kIQDoneButtonToolbarTag || toolbar.tag == kIQPreviousNextButtonToolbarTag))
{
textField.inputAccessoryView = nil;
}
}
}
#pragma mark previous/next/done functionality
/** previousAction. */
-(void)previousAction:(id)segmentedControl
{
//If user wants to play input Click sound.
if (_shouldPlayInputClicks)
{
//Play Input Click Sound.
[[UIDevice currentDevice] playInputClick];
}
if ([self canGoPrevious])
{
[self goPrevious];
}
}
/** nextAction. */
-(void)nextAction:(id)segmentedControl
{
//If user wants to play input Click sound.
if (_shouldPlayInputClicks)
{
//Play Input Click Sound.
[[UIDevice currentDevice] playInputClick];
}
if ([self canGoNext])
{
[self goNext];
}
}
/** doneAction. Resigning current textField. */
-(void)doneAction:(IQBarButtonItem*)barButton
{
//If user wants to play input Click sound.
if (_shouldPlayInputClicks)
{
//Play Input Click Sound.
[[UIDevice currentDevice] playInputClick];
}
[self resignFirstResponder];
}
#pragma mark - Tracking untracking
-(void)disableInViewControllerClass:(Class)disabledClass
{
[_disabledClasses addObject:disabledClass];
}
-(void)removeDisableInViewControllerClass:(Class)disabledClass
{
[_disabledClasses removeObject:disabledClass];
}
-(void)disableToolbarInViewControllerClass:(Class)toolbarDisabledClass
{
[_disabledToolbarClasses addObject:toolbarDisabledClass];
}
-(void)removeDisableToolbarInViewControllerClass:(Class)toolbarDisabledClass
{
[_disabledToolbarClasses removeObject:toolbarDisabledClass];
}
-(void)considerToolbarPreviousNextInViewClass:(Class)toolbarPreviousNextConsideredClass
{
[_toolbarPreviousNextConsideredClass addObject:toolbarPreviousNextConsideredClass];
}
-(void)removeConsiderToolbarPreviousNextInViewClass:(Class)toolbarPreviousNextConsideredClass
{
[_toolbarPreviousNextConsideredClass removeObject:toolbarPreviousNextConsideredClass];
}
@end
void _IQShowLog(NSString *logString)
{
#if IQKEYBOARDMANAGER_DEBUG
NSLog(@"IQKeyboardManager: %@",logString);
#endif
}