// // IQKeyboardManager.m // https://github.com/hackiftekhar/IQKeyboardManager // Copyright (c) 2013-16 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 "IQNSArray+Sort.h" #import "IQKeyboardManagerConstantsInternal.h" #import "IQUIScrollView+Additions.h" #import "IQUITextFieldView+Additions.h" #import "IQUIViewController+Additions.h" #import "IQPreviousNextView.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import NSInteger const kIQDoneButtonToolbarTag = -1002; NSInteger const kIQPreviousNextButtonToolbarTag = -1005; #define kIQCGPointInvalid CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX) @interface IQKeyboardManager() /*******************************************/ /** used to adjust contentInset of UITextView. */ @property(nonatomic, assign) UIEdgeInsets startingTextViewContentInsets; /** used to adjust scrollIndicatorInsets of UITextView. */ @property(nonatomic, assign) UIEdgeInsets startingTextViewScrollIndicatorInsets; /** used with textView to detect a textFieldView contentInset is changed or not. (Bug ID: #92)*/ @property(nonatomic, assign) BOOL isTextViewContentInsetChanged; /*******************************************/ /** To save UITextField/UITextView object voa textField/textView notifications. */ @property(nonatomic, weak) UIView *textFieldView; /** To save rootViewController.view.frame.origin. */ @property(nonatomic, assign) CGPoint topViewBeginOrigin; /** To save rootViewController */ @property(nonatomic, weak) UIViewController *rootViewController; /** To know if we have any pending request to adjust view position. */ @property(nonatomic, assign) BOOL hasPendingAdjustRequest; /*******************************************/ /** Variable to save lastScrollView that was scrolled. */ @property(nonatomic, weak) UIScrollView *lastScrollView; /** LastScrollView's initial contentInsets. */ @property(nonatomic, assign) UIEdgeInsets startingContentInsets; /** LastScrollView's initial scrollIndicatorInsets. */ @property(nonatomic, assign) UIEdgeInsets startingScrollIndicatorInsets; /** LastScrollView's initial contentOffset. */ @property(nonatomic, assign) CGPoint startingContentOffset; /*******************************************/ /** To save keyboard animation duration. */ @property(nonatomic, assign) CGFloat animationDuration; /** To mimic the keyboard animation */ @property(nonatomic, assign) NSInteger animationCurve; /*******************************************/ /** TapGesture to resign keyboard on view's touch. It's a readonly property and exposed only for adding/removing dependencies if your added gesture does have collision with this one */ @property(nonnull, nonatomic, strong, readwrite) UITapGestureRecognizer *resignFirstResponderGesture; /** moved distance to the top used to maintain distance between keyboard and textField. Most of the time this will be a positive value. */ @property(nonatomic, assign, readwrite) CGFloat movedDistance; /*******************************************/ @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *registeredClasses; @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *disabledDistanceHandlingClasses; @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *enabledDistanceHandlingClasses; @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *disabledToolbarClasses; @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *enabledToolbarClasses; @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *toolbarPreviousNextAllowedClasses; @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *disabledTouchResignedClasses; @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *enabledTouchResignedClasses; @property(nonatomic, strong, nonnull, readwrite) NSMutableSet *touchResignedGestureIgnoreClasses; /*******************************************/ @end @implementation IQKeyboardManager { @package /*******************************************/ /** To save keyboardWillShowNotification. Needed for enable keyboard functionality. */ NSNotification *_kbShowNotification; /** To save keyboard size. */ CGSize _kbSize; /*******************************************/ } //UIKeyboard handling @synthesize enable = _enable; @synthesize keyboardDistanceFromTextField = _keyboardDistanceFromTextField; //Keyboard Appearance handling @synthesize overrideKeyboardAppearance = _overrideKeyboardAppearance; @synthesize keyboardAppearance = _keyboardAppearance; //IQToolbar handling @synthesize enableAutoToolbar = _enableAutoToolbar; @synthesize toolbarManageBehaviour = _toolbarManageBehaviour; @synthesize shouldToolbarUsesTextFieldTintColor = _shouldToolbarUsesTextFieldTintColor; @synthesize toolbarTintColor = _toolbarTintColor; @synthesize toolbarBarTintColor = _toolbarBarTintColor; @dynamic shouldShowTextFieldPlaceholder; @synthesize shouldShowToolbarPlaceholder = _shouldShowToolbarPlaceholder; @synthesize placeholderFont = _placeholderFont; @synthesize placeholderColor = _placeholderColor; @synthesize placeholderButtonColor = _placeholderButtonColor; //Resign handling @synthesize shouldResignOnTouchOutside = _shouldResignOnTouchOutside; @synthesize resignFirstResponderGesture = _resignFirstResponderGesture; //Sound handling @synthesize shouldPlayInputClicks = _shouldPlayInputClicks; //Animation handling @synthesize layoutIfNeededOnUpdate = _layoutIfNeededOnUpdate; #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 { //Enabling IQKeyboardManager. Loading asynchronous on main thread [self performSelectorOnMainThread:@selector(sharedManager) withObject:nil waitUntilDone:NO]; } /* Singleton Object Initialization. */ -(instancetype)init { if (self = [super init]) { __weak typeof(self) weakSelf = self; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ __strong typeof(self) strongSelf = weakSelf; strongSelf.registeredClasses = [[NSMutableSet alloc] init]; [strongSelf registerAllNotifications]; //Creating gesture for @shouldResignOnTouchOutside. (Enhancement ID: #14) strongSelf.resignFirstResponderGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRecognized:)]; strongSelf.resignFirstResponderGesture.cancelsTouchesInView = NO; [strongSelf.resignFirstResponderGesture setDelegate:self]; strongSelf.resignFirstResponderGesture.enabled = strongSelf.shouldResignOnTouchOutside; strongSelf.topViewBeginOrigin = kIQCGPointInvalid; //Setting it's initial values strongSelf.animationDuration = 0.25; strongSelf.animationCurve = UIViewAnimationCurveEaseInOut; [self setEnable:YES]; [self setKeyboardDistanceFromTextField:10.0]; [self setShouldPlayInputClicks:YES]; [self setShouldResignOnTouchOutside:NO]; [self setOverrideKeyboardAppearance:NO]; [self setKeyboardAppearance:UIKeyboardAppearanceDefault]; [self setEnableAutoToolbar:YES]; [self setShouldShowToolbarPlaceholder:YES]; [self setToolbarManageBehaviour:IQAutoToolbarBySubviews]; [self setLayoutIfNeededOnUpdate:NO]; //Loading IQToolbar, IQTitleBarButtonItem, IQBarButtonItem to fix first time keyboard appearance delay (Bug ID: #550) { //If you experience exception breakpoint issue at below line then try these solutions https://stackoverflow.com/questions/27375640/all-exception-break-point-is-stopping-for-no-reason-on-simulator UITextField *view = [[UITextField alloc] init]; [view addDoneOnKeyboardWithTarget:nil action:nil]; [view addPreviousNextDoneOnKeyboardWithTarget:nil previousAction:nil nextAction:nil doneAction:nil]; } //Initializing disabled classes Set. strongSelf.disabledDistanceHandlingClasses = [[NSMutableSet alloc] initWithObjects:[UITableViewController class],[UIAlertController class], nil]; strongSelf.enabledDistanceHandlingClasses = [[NSMutableSet alloc] init]; strongSelf.disabledToolbarClasses = [[NSMutableSet alloc] initWithObjects:[UIAlertController class], nil]; strongSelf.enabledToolbarClasses = [[NSMutableSet alloc] init]; strongSelf.toolbarPreviousNextAllowedClasses = [[NSMutableSet alloc] initWithObjects:[UITableView class],[UICollectionView class],[IQPreviousNextView class], nil]; strongSelf.disabledTouchResignedClasses = [[NSMutableSet alloc] initWithObjects:[UIAlertController class], nil]; strongSelf.enabledTouchResignedClasses = [[NSMutableSet alloc] init]; strongSelf.touchResignedGestureIgnoreClasses = [[NSMutableSet alloc] initWithObjects:[UIControl class],[UINavigationBar class], nil]; [self setShouldToolbarUsesTextFieldTintColor:NO]; }); } return self; } /* Automatically called from the `+(void)load` method. */ + (IQKeyboardManager*)sharedManager { //Singleton instance static IQKeyboardManager *kbManager; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ kbManager = [[self alloc] init]; }); 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]; [self showLog:@"Enabled"]; } //If not disable, desable it. else if (enable == NO && _enable == YES) { //Sending a fake notification for keyboardWillHide to retain view's original position. [self keyboardWillHide:nil]; //Setting NO to _enable. _enable = enable; [self showLog:@"Disabled"]; } //If already disabled. else if (enable == NO && _enable == NO) { [self showLog:@"Already Disabled"]; } //If already enabled. else if (enable == YES && _enable == YES) { [self showLog:@"Already Enabled"]; } } -(BOOL)privateIsEnabled { BOOL enable = _enable; // IQEnableMode enableMode = _textFieldView.enableMode; // // if (enableMode == IQEnableModeEnabled) // { // enable = YES; // } // else if (enableMode == IQEnableModeDisabled) // { // enable = NO; // } // else { UIViewController *textFieldViewController = [_textFieldView viewContainingController]; if (textFieldViewController) { if (enable == NO) { //If viewController is kind of enable viewController class, then assuming it's enabled. for (Class enabledClass in _enabledDistanceHandlingClasses) { if ([textFieldViewController isKindOfClass:enabledClass]) { enable = YES; break; } } } if (enable) { //If viewController is kind of disable viewController class, then assuming it's disable. for (Class disabledClass in _disabledDistanceHandlingClasses) { if ([textFieldViewController isKindOfClass:disabledClass]) { enable = NO; break; } } //Special Controllers if (enable == YES) { NSString *classNameString = NSStringFromClass([textFieldViewController class]); //_UIAlertControllerTextFieldViewController if ([classNameString containsString:@"UIAlertController"] && [classNameString hasSuffix:@"TextFieldViewController"]) { enable = NO; } } } } } return enable; } -(BOOL)shouldShowTextFieldPlaceholder { return _shouldShowToolbarPlaceholder; } -(void)setShouldShowTextFieldPlaceholder:(BOOL)shouldShowTextFieldPlaceholder { _shouldShowToolbarPlaceholder = shouldShowTextFieldPlaceholder; } // Setting keyboard distance from text field. -(void)setKeyboardDistanceFromTextField:(CGFloat)keyboardDistanceFromTextField { //Can't be less than zero. Minimum is zero. _keyboardDistanceFromTextField = MAX(keyboardDistanceFromTextField, 0); [self showLog:[NSString stringWithFormat:@"keyboardDistanceFromTextField: %.2f",_keyboardDistanceFromTextField]]; } /** Enabling/disable gesture on touching. */ -(void)setShouldResignOnTouchOutside:(BOOL)shouldResignOnTouchOutside { [self showLog:[NSString stringWithFormat:@"shouldResignOnTouchOutside: %@",shouldResignOnTouchOutside?@"Yes":@"No"]]; _shouldResignOnTouchOutside = shouldResignOnTouchOutside; //Enable/Disable gesture recognizer (Enhancement ID: #14) [_resignFirstResponderGesture setEnabled:[self privateShouldResignOnTouchOutside]]; } -(BOOL)privateShouldResignOnTouchOutside { BOOL shouldResignOnTouchOutside = _shouldResignOnTouchOutside; UIView *textFieldView = _textFieldView; IQEnableMode enableMode = textFieldView.shouldResignOnTouchOutsideMode; if (enableMode == IQEnableModeEnabled) { shouldResignOnTouchOutside = YES; } else if (enableMode == IQEnableModeDisabled) { shouldResignOnTouchOutside = NO; } else { UIViewController *textFieldViewController = [textFieldView viewContainingController]; if (textFieldViewController) { if (shouldResignOnTouchOutside == NO) { //If viewController is kind of enable viewController class, then assuming shouldResignOnTouchOutside is enabled. for (Class enabledClass in _enabledTouchResignedClasses) { if ([textFieldViewController isKindOfClass:enabledClass]) { shouldResignOnTouchOutside = YES; break; } } } if (shouldResignOnTouchOutside) { //If viewController is kind of disable viewController class, then assuming shouldResignOnTouchOutside is disable. for (Class disabledClass in _disabledTouchResignedClasses) { if ([textFieldViewController isKindOfClass:disabledClass]) { shouldResignOnTouchOutside = NO; break; } } //Special Controllers if (shouldResignOnTouchOutside == YES) { NSString *classNameString = NSStringFromClass([textFieldViewController class]); //_UIAlertControllerTextFieldViewController if ([classNameString containsString:@"UIAlertController"] && [classNameString hasSuffix:@"TextFieldViewController"]) { shouldResignOnTouchOutside = NO; } } } } } return shouldResignOnTouchOutside; } /** Enable/disable autotoolbar. Adding and removing toolbar if required. */ -(void)setEnableAutoToolbar:(BOOL)enableAutoToolbar { _enableAutoToolbar = enableAutoToolbar; [self showLog:[NSString stringWithFormat:@"enableAutoToolbar: %@",enableAutoToolbar?@"Yes":@"No"]]; //If enabled then adding toolbar. if ([self privateIsEnableAutoToolbar] == YES) { [self addToolbarIfRequired]; } //Else removing toolbar. else { [self removeToolbarIfRequired]; } } -(BOOL)privateIsEnableAutoToolbar { BOOL enableAutoToolbar = _enableAutoToolbar; UIViewController *textFieldViewController = [_textFieldView viewContainingController]; if (textFieldViewController) { if (enableAutoToolbar == NO) { //If found any toolbar enabled classes then return. for (Class enabledToolbarClass in _enabledToolbarClasses) { if ([textFieldViewController isKindOfClass:enabledToolbarClass]) { enableAutoToolbar = YES; break; } } } if (enableAutoToolbar) { //If found any toolbar disabled classes then return. for (Class disabledToolbarClass in _disabledToolbarClasses) { if ([textFieldViewController isKindOfClass:disabledToolbarClass]) { enableAutoToolbar = NO; break; } } //Special Controllers if (enableAutoToolbar == YES) { NSString *classNameString = NSStringFromClass([textFieldViewController class]); //_UIAlertControllerTextFieldViewController if ([classNameString containsString:@"UIAlertController"] && [classNameString hasSuffix:@"TextFieldViewController"]) { enableAutoToolbar = NO; } } } } return enableAutoToolbar; } #pragma mark - Private Methods /** Getting keyWindow. */ -(UIWindow *)keyWindow { UIView *textFieldView = _textFieldView; if (textFieldView.window) { return textFieldView.window; } else { static __weak 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 && _keyWindow != originalKeyWindow) { _keyWindow = originalKeyWindow; } return _keyWindow; } } -(void)optimizedAdjustPosition { if (_hasPendingAdjustRequest == NO) { _hasPendingAdjustRequest = YES; __weak typeof(self) weakSelf = self; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self adjustPosition]; weakSelf.hasPendingAdjustRequest = NO; }]; } } /* Adjusting RootViewController's frame according to interface orientation. */ -(void)adjustPosition { UIView *textFieldView = _textFieldView; // Getting RootViewController. (Bug ID: #1, #4) UIViewController *rootController = _rootViewController; // Getting KeyWindow object. UIWindow *keyWindow = [self keyWindow]; // We are unable to get textField object while keyboard showing on UIWebView's textField. (Bug ID: #11) if (_hasPendingAdjustRequest == NO || textFieldView == nil || rootController == nil || keyWindow == nil) return; CFTimeInterval startTime = CACurrentMediaTime(); [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; // Converting Rectangle according to window bounds. CGRect textFieldViewRectInWindow = [[textFieldView superview] convertRect:textFieldView.frame toView:keyWindow]; CGRect textFieldViewRectInRootSuperview = [[textFieldView superview] convertRect:textFieldView.frame toView:rootController.view.superview]; // Getting RootView origin. CGPoint rootViewOrigin = rootController.view.frame.origin; //Maintain keyboardDistanceFromTextField CGFloat specialKeyboardDistanceFromTextField = textFieldView.keyboardDistanceFromTextField; { UISearchBar *searchBar = textFieldView.searchBar; if (searchBar) { specialKeyboardDistanceFromTextField = searchBar.keyboardDistanceFromTextField; } } CGFloat keyboardDistanceFromTextField = (specialKeyboardDistanceFromTextField == kIQUseDefaultKeyboardDistance)?_keyboardDistanceFromTextField:specialKeyboardDistanceFromTextField; CGSize kbSize = _kbSize; kbSize.height += keyboardDistanceFromTextField; CGFloat topLayoutGuide = rootController.view.layoutMargins.top+5; CGFloat move = 0; // +Move positive = textField is hidden. // -Move negative = textField is showing. // Calculating move position. Common for both normal and special cases. move = MIN(CGRectGetMinY(textFieldViewRectInRootSuperview)-topLayoutGuide, CGRectGetMaxY(textFieldViewRectInWindow)-(CGRectGetHeight(keyWindow.frame)-kbSize.height)); [self showLog:[NSString stringWithFormat:@"Need to move: %.2f",move]]; UIScrollView *superScrollView = nil; UIScrollView *superView = (UIScrollView*)[textFieldView superviewOfClassType:[UIScrollView class]]; //Getting UIScrollView whose scrolling is enabled. // (Bug ID: #285) while (superView) { if (superView.isScrollEnabled && superView.shouldIgnoreScrollingAdjustment == NO) { superScrollView = superView; break; } else { // Getting it's superScrollView. // (Enhancement ID: #21, #24) superView = (UIScrollView*)[superView 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) { [self showLog:[NSString stringWithFormat:@"Restoring %@ contentInset to : %@ and contentOffset to : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]]; __weak typeof(self) weakSelf = self; [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ __strong typeof(self) strongSelf = weakSelf; UIScrollView *strongLastScrollView = strongSelf.lastScrollView; [strongLastScrollView setContentInset:strongSelf.startingContentInsets]; strongLastScrollView.scrollIndicatorInsets = strongSelf.startingScrollIndicatorInsets; } completion:NULL]; if (_lastScrollView.shouldRestoreScrollViewContentOffset) { [_lastScrollView setContentOffset:_startingContentOffset animated:UIView.areAnimationsEnabled]; } _startingContentInsets = UIEdgeInsetsZero; _startingScrollIndicatorInsets = 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) { [self showLog:[NSString stringWithFormat:@"Restoring %@ contentInset to : %@ and contentOffset to : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]]; __weak typeof(self) weakSelf = self; [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ __strong typeof(self) strongSelf = weakSelf; UIScrollView *strongLastScrollView = strongSelf.lastScrollView; [strongLastScrollView setContentInset:strongSelf.startingContentInsets]; strongLastScrollView.scrollIndicatorInsets = strongSelf.startingScrollIndicatorInsets; } completion:NULL]; if (_lastScrollView.shouldRestoreScrollViewContentOffset) { [_lastScrollView setContentOffset:_startingContentOffset animated:UIView.areAnimationsEnabled]; } _lastScrollView = superScrollView; _startingContentInsets = superScrollView.contentInset; _startingScrollIndicatorInsets = superScrollView.scrollIndicatorInsets; _startingContentOffset = superScrollView.contentOffset; [self showLog:[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; _startingScrollIndicatorInsets = superScrollView.scrollIndicatorInsets; [self showLog:[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; 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) ) { UIScrollView *nextScrollView = nil; UIScrollView *tempScrollView = (UIScrollView*)[superScrollView superviewOfClassType:[UIScrollView class]]; //Getting UIScrollView whose scrolling is enabled. // (Bug ID: #285) while (tempScrollView) { if (tempScrollView.isScrollEnabled && tempScrollView.shouldIgnoreScrollingAdjustment == NO) { nextScrollView = tempScrollView; break; } else { // Getting it's superScrollView. // (Enhancement ID: #21, #24) tempScrollView = (UIScrollView*)[tempScrollView superviewOfClassType:[UIScrollView class]]; } } //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); //[textFieldView isKindOfClass:[UITextView class]] If is a UITextView type //[superScrollView superviewOfClassType:[UIScrollView class]] == nil If processing scrollView is last scrollView in upper hierarchy (there is no other scrollView upper hierarchy.) //shouldOffsetY >= 0 shouldOffsetY must be greater than in order to keep distance from navigationBar (Bug ID: #92) if ([textFieldView isKindOfClass:[UITextView class]] && nextScrollView == nil && (shouldOffsetY >= 0)) { // Converting Rectangle according to window bounds. CGRect currentTextFieldViewRect = [[textFieldView superview] convertRect:textFieldView.frame toView:keyWindow]; //Calculating expected fix distance which needs to be managed from navigation bar CGFloat expectedFixDistance = CGRectGetMinY(currentTextFieldViewRect) - topLayoutGuide; //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:^{ [self showLog:[NSString stringWithFormat:@"Adjusting %.2f to %@ ContentOffset",(superScrollView.contentOffset.y-shouldOffsetY),[superScrollView _IQDescription]]]; [self showLog:[NSString stringWithFormat:@"Remaining Move: %.2f",move]]; superScrollView.contentOffset = CGPointMake(superScrollView.contentOffset.x, shouldOffsetY); } completion:NULL]; // Getting next lastView & superScrollView. lastView = superScrollView; superScrollView = nextScrollView; } //Updating contentInset { CGRect lastScrollViewRect = [[_lastScrollView superview] convertRect:_lastScrollView.frame toView:keyWindow]; CGFloat bottom = (kbSize.height-keyboardDistanceFromTextField)-(CGRectGetHeight(keyWindow.frame)-CGRectGetMaxY(lastScrollViewRect)); // 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); [self showLog:[NSString stringWithFormat:@"%@ old ContentInset : %@",[_lastScrollView _IQDescription], NSStringFromUIEdgeInsets(_lastScrollView.contentInset)]]; __weak typeof(self) weakSelf = self; [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ __strong typeof(self) strongSelf = weakSelf; UIScrollView *strongLastScrollView = strongSelf.lastScrollView; strongLastScrollView.contentInset = movedInsets; UIEdgeInsets newInset = strongLastScrollView.scrollIndicatorInsets; newInset.bottom = movedInsets.bottom; strongLastScrollView.scrollIndicatorInsets = newInset; } completion:NULL]; [self showLog:[NSString stringWithFormat:@"%@ new ContentInset : %@",[_lastScrollView _IQDescription], NSStringFromUIEdgeInsets(_lastScrollView.contentInset)]]; } } //Going ahead. No else if. } { //Special case for UITextView(Readjusting textView.contentInset when textView hight is too big to fit on screen) //_lastScrollView If not having inside any scrollView, (now contentInset manages the full screen textView. //[textFieldView isKindOfClass:[UITextView class]] If is a UITextView type if ([textFieldView isKindOfClass:[UITextView class]]) { UITextView *textView = (UITextView*)textFieldView; CGFloat keyboardYPosition = CGRectGetHeight(keyWindow.frame)-(kbSize.height-keyboardDistanceFromTextField); CGRect rootSuperViewFrameInWindow = [rootController.view.superview convertRect:rootController.view.superview.bounds toView:keyWindow]; CGFloat keyboardOverlapping = CGRectGetMaxY(rootSuperViewFrameInWindow) - keyboardYPosition; CGFloat textViewHeight = MIN(CGRectGetHeight(textFieldView.frame), (CGRectGetHeight(rootSuperViewFrameInWindow)-topLayoutGuide-keyboardOverlapping)); if (textFieldView.frame.size.height-textView.contentInset.bottom>textViewHeight) { __weak typeof(self) weakSelf = self; [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ __strong typeof(self) strongSelf = weakSelf; UIView *strongTextFieldView = strongSelf.textFieldView; [self showLog:[NSString stringWithFormat:@"%@ Old UITextView.contentInset : %@",[strongTextFieldView _IQDescription], NSStringFromUIEdgeInsets(textView.contentInset)]]; //_isTextViewContentInsetChanged, If frame is not change by library in past, then saving user textView properties (Bug ID: #92) if (strongSelf.isTextViewContentInsetChanged == NO) { strongSelf.startingTextViewContentInsets = textView.contentInset; strongSelf.startingTextViewScrollIndicatorInsets = textView.scrollIndicatorInsets; } UIEdgeInsets newContentInset = textView.contentInset; newContentInset.bottom = strongTextFieldView.frame.size.height-textViewHeight; textView.contentInset = newContentInset; textView.scrollIndicatorInsets = newContentInset; strongSelf.isTextViewContentInsetChanged = YES; [self showLog:[NSString stringWithFormat:@"%@ New UITextView.contentInset : %@",[strongTextFieldView _IQDescription], NSStringFromUIEdgeInsets(textView.contentInset)]]; } completion:NULL]; } } { __weak typeof(self) weakSelf = self; // +Positive or zero. if (move>=0) { rootViewOrigin.y -= move; // From now prevent keyboard manager to slide up the rootView to more than keyboard height. (Bug ID: #93) rootViewOrigin.y = MAX(rootViewOrigin.y, MIN(0, -(kbSize.height-keyboardDistanceFromTextField))); [self showLog:@"Moving Upward"]; // Setting adjusted rootViewOrigin.ty //Used UIViewAnimationOptionBeginFromCurrentState to minimize strange animations. [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ __strong typeof(self) strongSelf = weakSelf; // Setting it's new frame CGRect rect = rootController.view.frame; rect.origin = rootViewOrigin; rootController.view.frame = rect; //Animating content if needed (Bug ID: #204) if (strongSelf.layoutIfNeededOnUpdate) { //Animating content (Bug ID: #160) [rootController.view setNeedsLayout]; [rootController.view layoutIfNeeded]; } [self showLog:[NSString stringWithFormat:@"Set %@ origin to : %@",[rootController _IQDescription],NSStringFromCGPoint(rootViewOrigin)]]; } completion:NULL]; _movedDistance = (_topViewBeginOrigin.y-rootViewOrigin.y); } // -Negative else { CGFloat disturbDistance = rootController.view.frame.origin.y-_topViewBeginOrigin.y; // disturbDistance Negative = frame disturbed. Pull Request #3 // disturbDistance positive = frame not disturbed. if(disturbDistance<=0) { rootViewOrigin.y -= MAX(move, disturbDistance); [self showLog:@"Moving Downward"]; // Setting adjusted rootViewRect //Used UIViewAnimationOptionBeginFromCurrentState to minimize strange animations. [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ __strong typeof(self) strongSelf = weakSelf; // Setting it's new frame CGRect rect = rootController.view.frame; rect.origin = rootViewOrigin; rootController.view.frame = rect; //Animating content if needed (Bug ID: #204) if (strongSelf.layoutIfNeededOnUpdate) { //Animating content (Bug ID: #160) [rootController.view setNeedsLayout]; [rootController.view layoutIfNeeded]; } [self showLog:[NSString stringWithFormat:@"Set %@ origin to : %@",[rootController _IQDescription],NSStringFromCGPoint(rootViewOrigin)]]; } completion:NULL]; _movedDistance = (_topViewBeginOrigin.y-rootController.view.frame.origin.y); } } } } CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; } -(void)restorePosition { _hasPendingAdjustRequest = NO; // Setting rootViewController frame to it's original position. // (Bug ID: #18) if (_rootViewController && CGPointEqualToPoint(_topViewBeginOrigin, kIQCGPointInvalid) == false) { __weak typeof(self) weakSelf = self; //Used UIViewAnimationOptionBeginFromCurrentState to minimize strange animations. [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ __strong typeof(self) strongSelf = weakSelf; UIViewController *strongRootController = strongSelf.rootViewController; { [strongSelf showLog:[NSString stringWithFormat:@"Restoring %@ origin to : %@",[strongRootController _IQDescription],NSStringFromCGPoint(strongSelf.topViewBeginOrigin)]]; //Restoring CGRect rect = strongRootController.view.frame; rect.origin = strongSelf.topViewBeginOrigin; strongRootController.view.frame = rect; strongSelf.movedDistance = 0; //Animating content if needed (Bug ID: #204) if (strongSelf.layoutIfNeededOnUpdate) { //Animating content (Bug ID: #160) [strongRootController.view setNeedsLayout]; [strongRootController.view layoutIfNeeded]; } } } completion:NULL]; _rootViewController = nil; } } #pragma mark - Public Methods /* Refreshes textField/textView position if any external changes is explicitly made by user. */ - (void)reloadLayoutIfNeeded { if ([self privateIsEnabled] == YES) { UIView *textFieldView = _textFieldView; if (textFieldView && _keyboardShowing == YES && CGPointEqualToPoint(_topViewBeginOrigin, kIQCGPointInvalid) == false && [textFieldView isAlertViewTextField] == NO) { [self optimizedAdjustPosition]; } } } #pragma mark - UIKeyboad Notification methods /* UIKeyboardWillShowNotification. */ -(void)keyboardWillShow:(NSNotification*)aNotification { _kbShowNotification = aNotification; // Boolean to know keyboard is showing/hiding _keyboardShowing = YES; // Getting keyboard animation. NSInteger curve = [[aNotification userInfo][UIKeyboardAnimationCurveUserInfoKey] integerValue]; _animationCurve = curve<<16; // Getting keyboard animation duration CGFloat duration = [[aNotification userInfo][UIKeyboardAnimationDurationUserInfoKey] floatValue]; //Saving animation duration if (duration != 0.0) _animationDuration = duration; CGSize oldKBSize = _kbSize; // Getting UIKeyboardSize. CGRect kbFrame = [[aNotification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGRect screenSize = [[UIScreen mainScreen] bounds]; //Calculating actual keyboard displayed size, keyboard frame may be different when hardware keyboard is attached (Bug ID: #469) (Bug ID: #381) CGRect intersectRect = CGRectIntersection(kbFrame, screenSize); if (CGRectIsNull(intersectRect)) { _kbSize = CGSizeMake(screenSize.size.width, 0); } else { _kbSize = intersectRect.size; } if ([self privateIsEnabled] == NO) return; CFTimeInterval startTime = CACurrentMediaTime(); [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; UIView *textFieldView = _textFieldView; if (textFieldView && CGPointEqualToPoint(_topViewBeginOrigin, kIQCGPointInvalid)) // (Bug ID: #5) { // keyboard is not showing(At the beginning only). We should save rootViewRect. UIViewController *rootController = [textFieldView parentContainerViewController]; _rootViewController = rootController; _topViewBeginOrigin = rootController.view.frame.origin; [self showLog:[NSString stringWithFormat:@"Saving %@ beginning origin: %@",[rootController _IQDescription] ,NSStringFromCGPoint(_topViewBeginOrigin)]]; } //If last restored keyboard size is different(any orientation accure), then refresh. otherwise not. if (!CGSizeEqualToSize(_kbSize, oldKBSize)) { //If _textFieldView is inside UIAlertView 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 (_keyboardShowing == YES && textFieldView && [textFieldView isAlertViewTextField] == NO) { [self optimizedAdjustPosition]; } } CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; } /* UIKeyboardDidShowNotification. */ - (void)keyboardDidShow:(NSNotification*)aNotification { if ([self privateIsEnabled] == NO) return; CFTimeInterval startTime = CACurrentMediaTime(); [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; UIView *textFieldView = _textFieldView; // Getting topMost ViewController. UIViewController *controller = [textFieldView topMostController]; //If _textFieldView viewController is presented as formSheet, then adjustPosition again because iOS internally update formSheet frame on keyboardShown. (Bug ID: #37, #74, #76) if (_keyboardShowing == YES && textFieldView && (controller.modalPresentationStyle == UIModalPresentationFormSheet || controller.modalPresentationStyle == UIModalPresentationPageSheet) && [textFieldView isAlertViewTextField] == NO) { [self optimizedAdjustPosition]; } CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; } /* 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) _kbShowNotification = nil; // Boolean to know keyboard is showing/hiding _keyboardShowing = NO; // Getting keyboard animation duration CGFloat aDuration = [[aNotification userInfo][UIKeyboardAnimationDurationUserInfoKey] floatValue]; if (aDuration!= 0.0f) { _animationDuration = aDuration; } //If not enabled then do nothing. if ([self privateIsEnabled] == NO) return; CFTimeInterval startTime = CACurrentMediaTime(); [self showLog:[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; //Restoring the contentOffset of the lastScrollView if (_lastScrollView) { __weak typeof(self) weakSelf = self; [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ __strong typeof(self) strongSelf = weakSelf; strongSelf.lastScrollView.contentInset = strongSelf.startingContentInsets; strongSelf.lastScrollView.scrollIndicatorInsets = strongSelf.startingScrollIndicatorInsets; if (strongSelf.lastScrollView.shouldRestoreScrollViewContentOffset) { strongSelf.lastScrollView.contentOffset = strongSelf.startingContentOffset; } [self showLog:[NSString stringWithFormat:@"Restoring %@ contentInset to : %@ and contentOffset to : %@",[strongSelf.lastScrollView _IQDescription],NSStringFromUIEdgeInsets(strongSelf.startingContentInsets),NSStringFromCGPoint(strongSelf.startingContentOffset)]]; // TODO: restore scrollView state // This is temporary solution. Have to implement the save and restore scrollView state UIScrollView *superscrollView = strongSelf.lastScrollView; do { 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 0) { return YES; } else { 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]; //Getting index of current textField. NSUInteger index = [textFields indexOfObject:_textFieldView]; //If it is not last textField. then it's next object becomeFirstResponder. if (index != NSNotFound && index < textFields.count-1) { return YES; } else { return NO; } } /** Navigate to previous responder textField/textView. */ -(BOOL)goPrevious { //Getting all responder view's. NSArray *textFields = [self responderViews]; //Getting index of current textField. NSUInteger index = [textFields indexOfObject:_textFieldView]; //If it is not first textField. then it's previous object becomeFirstResponder. if (index != NSNotFound && index > 0) { UITextField *nextTextField = textFields[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]; [self showLog:[NSString stringWithFormat:@"Refuses to become first responder: %@",[nextTextField _IQDescription]]]; } return isAcceptAsFirstResponder; } else { return NO; } } /** Navigate to next responder textField/textView. */ -(BOOL)goNext { //Getting all responder view's. NSArray *textFields = [self responderViews]; //Getting index of current textField. NSUInteger index = [textFields indexOfObject:_textFieldView]; //If it is not last textField. then it's next object becomeFirstResponder. if (index != NSNotFound && index < textFields.count-1) { UITextField *nextTextField = textFields[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]; [self showLog:[NSString stringWithFormat:@"Refuses to become first responder: %@",[nextTextField _IQDescription]]]; } return isAcceptAsFirstResponder; } else { return NO; } } #pragma mark AutoToolbar methods /** Get all UITextField/UITextView siblings of textFieldView. */ -(NSArray*)responderViews { UIView *superConsideredView; UIView *textFieldView = _textFieldView; //If find any consider responderView in it's upper hierarchy then will get deepResponderView. for (Class consideredClass in _toolbarPreviousNextAllowedClasses) { superConsideredView = [textFieldView superviewOfClassType:consideredClass]; if (superConsideredView) 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 { CFTimeInterval startTime = CACurrentMediaTime(); [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; // Getting all the sibling textFields. NSArray *siblings = [self responderViews]; [self showLog:[NSString stringWithFormat:@"Found %lu responder sibling(s)",(unsigned long)siblings.count]]; UIView *textFieldView = _textFieldView; //Either there is no inputAccessoryView or if accessoryView is not appropriate for current situation(There is Previous/Next/Done toolbar). //setInputAccessoryView: check (Bug ID: #307) if ([textFieldView respondsToSelector:@selector(setInputAccessoryView:)]) { if ([textFieldView inputAccessoryView] == nil || [[textFieldView inputAccessoryView] tag] == kIQPreviousNextButtonToolbarTag || [[textFieldView inputAccessoryView] tag] == kIQDoneButtonToolbarTag) { UITextField *textField = (UITextField*)textFieldView; // If only one object is found, then adding only Done button. if ((siblings.count==1 && self.previousNextDisplayMode == IQPreviousNextDisplayModeDefault) || self.previousNextDisplayMode == IQPreviousNextDisplayModeAlwaysHide) { //Supporting Custom Done button image (Enhancement ID: #366) if (_toolbarDoneBarButtonItemImage) { [textField addRightButtonOnKeyboardWithImage:_toolbarDoneBarButtonItemImage target:self action:@selector(doneAction:) shouldShowPlaceholder:_shouldShowToolbarPlaceholder]; } //Supporting Custom Done button text (Enhancement ID: #209, #411, Bug ID: #376) else if (_toolbarDoneBarButtonItemText) { [textField addRightButtonOnKeyboardWithText:_toolbarDoneBarButtonItemText target:self action:@selector(doneAction:) shouldShowPlaceholder:_shouldShowToolbarPlaceholder]; } else { //Now adding textField placeholder text as title of IQToolbar (Enhancement ID: #27) [textField addDoneOnKeyboardWithTarget:self action:@selector(doneAction:) shouldShowPlaceholder:_shouldShowToolbarPlaceholder]; } textField.inputAccessoryView.tag = kIQDoneButtonToolbarTag; // (Bug ID: #78) } //If there is multiple siblings of textField else if ((siblings.count && self.previousNextDisplayMode == IQPreviousNextDisplayModeDefault) || self.previousNextDisplayMode == IQPreviousNextDisplayModeAlwaysShow) { //Supporting Custom Done button image (Enhancement ID: #366) if (_toolbarDoneBarButtonItemImage) { [textField addPreviousNextRightOnKeyboardWithTarget:self rightButtonImage:_toolbarDoneBarButtonItemImage previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) rightButtonAction:@selector(doneAction:) shouldShowPlaceholder:_shouldShowToolbarPlaceholder]; } //Supporting Custom Done button text (Enhancement ID: #209, #411, Bug ID: #376) else if (_toolbarDoneBarButtonItemText) { [textField addPreviousNextRightOnKeyboardWithTarget:self rightButtonTitle:_toolbarDoneBarButtonItemText previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) rightButtonAction:@selector(doneAction:) shouldShowPlaceholder:_shouldShowToolbarPlaceholder]; } else { //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:_shouldShowToolbarPlaceholder]; } textField.inputAccessoryView.tag = kIQPreviousNextButtonToolbarTag; // (Bug ID: #78) } IQToolbar *toolbar = textField.keyboardToolbar; //Bar style according to keyboard appearance if ([textField respondsToSelector:@selector(keyboardAppearance)]) { switch ([textField keyboardAppearance]) { case UIKeyboardAppearanceAlert: { toolbar.barStyle = UIBarStyleBlack; [toolbar setTintColor:[UIColor whiteColor]]; [toolbar setBarTintColor:nil]; } break; default: { toolbar.barStyle = UIBarStyleDefault; toolbar.barTintColor = _toolbarBarTintColor; //Setting toolbar tintColor // (Enhancement ID: #30) if (_shouldToolbarUsesTextFieldTintColor) { toolbar.tintColor = [textField tintColor]; } else if (_toolbarTintColor) { toolbar.tintColor = _toolbarTintColor; } else { toolbar.tintColor = [UIColor blackColor]; } } break; } //If need to show placeholder if (_shouldShowToolbarPlaceholder && textField.shouldHideToolbarPlaceholder == NO) { //Updating placeholder //(Bug ID: #148, #272) if (toolbar.titleBarButton.title == nil || [toolbar.titleBarButton.title isEqualToString:textField.drawingToolbarPlaceholder] == NO) { [toolbar.titleBarButton setTitle:textField.drawingToolbarPlaceholder]; } //Setting toolbar title font. // (Enhancement ID: #30) if (_placeholderFont && [_placeholderFont isKindOfClass:[UIFont class]]) { [toolbar.titleBarButton setTitleFont:_placeholderFont]; } //Setting toolbar title color. // (Enhancement ID: #880) if (_placeholderColor && [_placeholderColor isKindOfClass:[UIColor class]]) { [toolbar.titleBarButton setTitleColor:_placeholderColor]; } //Setting toolbar button title color. // (Enhancement ID: #880) if (_placeholderButtonColor && [_placeholderButtonColor isKindOfClass:[UIColor class]]) { [toolbar.titleBarButton setSelectableTitleColor:_placeholderButtonColor]; } } else { //Updating placeholder //(Bug ID: #272) toolbar.titleBarButton.title = nil; } } //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.firstObject == textField) { if (siblings.count == 1) { textField.keyboardToolbar.previousBarButton.enabled = NO; textField.keyboardToolbar.nextBarButton.enabled = NO; } else { textField.keyboardToolbar.previousBarButton.enabled = NO; textField.keyboardToolbar.nextBarButton.enabled = YES; } } // If lastTextField then next should not be enaled. else if ([siblings lastObject] == textField) { textField.keyboardToolbar.previousBarButton.enabled = YES; textField.keyboardToolbar.nextBarButton.enabled = NO; } else { textField.keyboardToolbar.previousBarButton.enabled = YES; textField.keyboardToolbar.nextBarButton.enabled = YES; } } } CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; } /** Remove any toolbar if it is IQToolbar. */ -(void)removeToolbarIfRequired // (Bug ID: #18) { CFTimeInterval startTime = CACurrentMediaTime(); [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; // Getting all the sibling textFields. NSArray *siblings = [self responderViews]; [self showLog:[NSString stringWithFormat:@"Found %lu responder sibling(s)",(unsigned long)siblings.count]]; for (UITextField *textField in siblings) { UIView *toolbar = [textField inputAccessoryView]; // (Bug ID: #78) //setInputAccessoryView: check (Bug ID: #307) if ([textField respondsToSelector:@selector(setInputAccessoryView:)] && ([toolbar isKindOfClass:[IQToolbar class]] && (toolbar.tag == kIQDoneButtonToolbarTag || toolbar.tag == kIQPreviousNextButtonToolbarTag))) { textField.inputAccessoryView = nil; [textField reloadInputViews]; } } CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; } /** reloadInputViews to reload toolbar buttons enable/disable state on the fly Enhancement ID #434. */ - (void)reloadInputViews { //If enabled then adding toolbar. if ([self privateIsEnableAutoToolbar] == YES) { [self addToolbarIfRequired]; } //Else removing toolbar. else { [self removeToolbarIfRequired]; } } #pragma mark previous/next/done functionality /** previousAction. */ -(void)previousAction:(IQBarButtonItem*)barButton { //If user wants to play input Click sound. Then Play Input Click Sound. if (_shouldPlayInputClicks) { [[UIDevice currentDevice] playInputClick]; } if ([self canGoPrevious]) { UIView *currentTextFieldView = _textFieldView; BOOL isAcceptAsFirstResponder = [self goPrevious]; NSInvocation *invocation = barButton.invocation; //Handling search bar special case { UISearchBar *searchBar = currentTextFieldView.searchBar; if (searchBar) { invocation = searchBar.keyboardToolbar.previousBarButton.invocation; } } if (isAcceptAsFirstResponder == YES && barButton.invocation) { if (barButton.invocation.methodSignature.numberOfArguments > 2) { [barButton.invocation setArgument:¤tTextFieldView atIndex:2]; } [barButton.invocation invoke]; } } } /** nextAction. */ -(void)nextAction:(IQBarButtonItem*)barButton { //If user wants to play input Click sound. Then Play Input Click Sound. if (_shouldPlayInputClicks) { [[UIDevice currentDevice] playInputClick]; } if ([self canGoNext]) { UIView *currentTextFieldView = _textFieldView; BOOL isAcceptAsFirstResponder = [self goNext]; NSInvocation *invocation = barButton.invocation; //Handling search bar special case { UISearchBar *searchBar = currentTextFieldView.searchBar; if (searchBar) { invocation = searchBar.keyboardToolbar.nextBarButton.invocation; } } if (isAcceptAsFirstResponder == YES && barButton.invocation) { if (barButton.invocation.methodSignature.numberOfArguments > 2) { [barButton.invocation setArgument:¤tTextFieldView atIndex:2]; } [barButton.invocation invoke]; } } } /** doneAction. Resigning current textField. */ -(void)doneAction:(IQBarButtonItem*)barButton { //If user wants to play input Click sound. Then Play Input Click Sound. if (_shouldPlayInputClicks) { [[UIDevice currentDevice] playInputClick]; } UIView *currentTextFieldView = _textFieldView; BOOL isResignedFirstResponder = [self resignFirstResponder]; NSInvocation *invocation = barButton.invocation; //Handling search bar special case { UISearchBar *searchBar = currentTextFieldView.searchBar; if (searchBar) { invocation = searchBar.keyboardToolbar.doneBarButton.invocation; } } if (isResignedFirstResponder == YES && barButton.invocation) { if (barButton.invocation.methodSignature.numberOfArguments > 2) { [barButton.invocation setArgument:¤tTextFieldView atIndex:2]; } [barButton.invocation invoke]; } } #pragma mark - Customised textField/textView support. /** Add customised Notification for third party customised TextField/TextView. */ -(void)registerTextFieldViewClass:(nonnull Class)aClass didBeginEditingNotificationName:(nonnull NSString *)didBeginEditingNotificationName didEndEditingNotificationName:(nonnull NSString *)didEndEditingNotificationName { [_registeredClasses addObject:aClass]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidBeginEditing:) name:didBeginEditingNotificationName object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidEndEditing:) name:didEndEditingNotificationName object:nil]; } /** Remove customised Notification for third party customised TextField/TextView. */ -(void)unregisterTextFieldViewClass:(nonnull Class)aClass didBeginEditingNotificationName:(nonnull NSString *)didBeginEditingNotificationName didEndEditingNotificationName:(nonnull NSString *)didEndEditingNotificationName { [_registeredClasses removeObject:aClass]; [[NSNotificationCenter defaultCenter] removeObserver:self name:didBeginEditingNotificationName object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:didEndEditingNotificationName object:nil]; } -(void)registerAllNotifications { // Registering for keyboard notification. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification 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 UITextField notification. [self registerTextFieldViewClass:[UITextField class] didBeginEditingNotificationName:UITextFieldTextDidBeginEditingNotification didEndEditingNotificationName:UITextFieldTextDidEndEditingNotification]; // Registering for UITextView notification. [self registerTextFieldViewClass:[UITextView class] didBeginEditingNotificationName:UITextViewTextDidBeginEditingNotification didEndEditingNotificationName:UITextViewTextDidEndEditingNotification]; // Registering for orientation changes notification [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willChangeStatusBarOrientation:) name:UIApplicationWillChangeStatusBarOrientationNotification object:[UIApplication sharedApplication]]; } -(void)unregisterAllNotifications { // Unregistering for keyboard notification. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidHideNotification object:nil]; // Unregistering for UITextField notification. [self unregisterTextFieldViewClass:[UITextField class] didBeginEditingNotificationName:UITextFieldTextDidBeginEditingNotification didEndEditingNotificationName:UITextFieldTextDidEndEditingNotification]; // Unregistering for UITextView notification. [self unregisterTextFieldViewClass:[UITextView class] didBeginEditingNotificationName:UITextViewTextDidBeginEditingNotification didEndEditingNotificationName:UITextViewTextDidEndEditingNotification]; // Unregistering for orientation changes notification [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillChangeStatusBarOrientationNotification object:[UIApplication sharedApplication]]; } -(void)showLog:(NSString*)logString { if (_enableDebugging) { NSLog(@"IQKeyboardManager: %@",logString); } } @end