New file |
| | |
| | | // |
| | | // 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 <QuartzCore/CABase.h> |
| | | |
| | | #import <objc/runtime.h> |
| | | |
| | | #import <UIKit/UIAlertController.h> |
| | | #import <UIKit/UISearchBar.h> |
| | | #import <UIKit/UIScreen.h> |
| | | #import <UIKit/UINavigationBar.h> |
| | | #import <UIKit/UITapGestureRecognizer.h> |
| | | #import <UIKit/UITextField.h> |
| | | #import <UIKit/UITextView.h> |
| | | #import <UIKit/UITableViewController.h> |
| | | #import <UIKit/UICollectionViewController.h> |
| | | #import <UIKit/UINavigationController.h> |
| | | #import <UIKit/UITouch.h> |
| | | #import <UIKit/UIWindow.h> |
| | | #import <UIKit/NSLayoutConstraint.h> |
| | | |
| | | |
| | | NSInteger const kIQDoneButtonToolbarTag = -1002; |
| | | NSInteger const kIQPreviousNextButtonToolbarTag = -1005; |
| | | |
| | | #define kIQCGPointInvalid CGPointMake(CGFLOAT_MAX, CGFLOAT_MAX) |
| | | |
| | | @interface IQKeyboardManager()<UIGestureRecognizerDelegate> |
| | | |
| | | /*******************************************/ |
| | | |
| | | /** 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<Class> *registeredClasses; |
| | | |
| | | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *disabledDistanceHandlingClasses; |
| | | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *enabledDistanceHandlingClasses; |
| | | |
| | | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *disabledToolbarClasses; |
| | | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *enabledToolbarClasses; |
| | | |
| | | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *toolbarPreviousNextAllowedClasses; |
| | | |
| | | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *disabledTouchResignedClasses; |
| | | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *enabledTouchResignedClasses; |
| | | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *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<superscrollView.contentOffset.y) |
| | | { |
| | | superscrollView.contentOffset = CGPointMake(superscrollView.contentOffset.x, minimumY); |
| | | |
| | | [self showLog:[NSString stringWithFormat:@"Restoring %@ contentOffset to : %@",[superscrollView _IQDescription],NSStringFromCGPoint(superscrollView.contentOffset)]]; |
| | | } |
| | | } while ((superscrollView = (UIScrollView*)[superscrollView superviewOfClassType:[UIScrollView class]])); |
| | | |
| | | } completion:NULL]; |
| | | } |
| | | |
| | | [self restorePosition]; |
| | | |
| | | //Reset all values |
| | | _lastScrollView = nil; |
| | | _kbSize = CGSizeZero; |
| | | _startingContentInsets = UIEdgeInsetsZero; |
| | | _startingScrollIndicatorInsets = UIEdgeInsetsZero; |
| | | _startingContentOffset = CGPointZero; |
| | | |
| | | CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; |
| | | } |
| | | |
| | | /* UIKeyboardDidHideNotification. So topViewBeginRect can be set to CGRectZero. */ |
| | | - (void)keyboardDidHide:(NSNotification*)aNotification |
| | | { |
| | | CFTimeInterval startTime = CACurrentMediaTime(); |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; |
| | | |
| | | _topViewBeginOrigin = kIQCGPointInvalid; |
| | | |
| | | _kbSize = CGSizeZero; |
| | | |
| | | CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; |
| | | } |
| | | |
| | | #pragma mark - UITextFieldView Delegate methods |
| | | /** UITextFieldTextDidBeginEditingNotification, UITextViewTextDidBeginEditingNotification. Fetching UITextFieldView object. */ |
| | | -(void)textFieldViewDidBeginEditing:(NSNotification*)notification |
| | | { |
| | | CFTimeInterval startTime = CACurrentMediaTime(); |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; |
| | | |
| | | // Getting object |
| | | _textFieldView = notification.object; |
| | | |
| | | UIView *textFieldView = _textFieldView; |
| | | |
| | | if (_overrideKeyboardAppearance == YES) |
| | | { |
| | | UITextField *textField = (UITextField*)textFieldView; |
| | | |
| | | if ([textField respondsToSelector:@selector(keyboardAppearance)]) |
| | | { |
| | | //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]; |
| | | } |
| | | } |
| | | } |
| | | |
| | | //If autoToolbar enable, then add toolbar on all the UITextField/UITextView's if required. |
| | | if ([self privateIsEnableAutoToolbar]) |
| | | { |
| | | //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) |
| | | { |
| | | __weak typeof(self) weakSelf = self; |
| | | |
| | | [UIView animateWithDuration:0.00001 delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ |
| | | [self addToolbarIfRequired]; |
| | | } completion:^(BOOL finished) { |
| | | |
| | | __strong typeof(self) strongSelf = weakSelf; |
| | | |
| | | //On textView toolbar didn't appear on first time, so forcing textView to reload it's inputViews. |
| | | [strongSelf.textFieldView reloadInputViews]; |
| | | }]; |
| | | } |
| | | //Else adding toolbar |
| | | else |
| | | { |
| | | [self addToolbarIfRequired]; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | [self removeToolbarIfRequired]; |
| | | } |
| | | |
| | | //Adding Geture recognizer to window (Enhancement ID: #14) |
| | | [_resignFirstResponderGesture setEnabled:[self privateShouldResignOnTouchOutside]]; |
| | | [textFieldView.window addGestureRecognizer:_resignFirstResponderGesture]; |
| | | |
| | | if ([self privateIsEnabled] == YES) |
| | | { |
| | | if (CGPointEqualToPoint(_topViewBeginOrigin, kIQCGPointInvalid)) // (Bug ID: #5) |
| | | { |
| | | // keyboard is not showing(At the beginning only). |
| | | UIViewController *rootController = [textFieldView parentContainerViewController]; |
| | | _rootViewController = rootController; |
| | | _topViewBeginOrigin = rootController.view.frame.origin; |
| | | |
| | | [self showLog:[NSString stringWithFormat:@"Saving %@ beginning origin: %@",[rootController _IQDescription], NSStringFromCGPoint(_topViewBeginOrigin)]]; |
| | | } |
| | | |
| | | //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) |
| | | { |
| | | // keyboard is already showing. adjust frame. |
| | | [self optimizedAdjustPosition]; |
| | | } |
| | | } |
| | | |
| | | // if ([textFieldView isKindOfClass:[UITextField class]]) |
| | | // { |
| | | // [(UITextField*)textFieldView addTarget:self action:@selector(editingDidEndOnExit:) forControlEvents:UIControlEventEditingDidEndOnExit]; |
| | | // } |
| | | |
| | | CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; |
| | | } |
| | | |
| | | /** UITextFieldTextDidEndEditingNotification, UITextViewTextDidEndEditingNotification. Removing fetched object. */ |
| | | -(void)textFieldViewDidEndEditing:(NSNotification*)notification |
| | | { |
| | | CFTimeInterval startTime = CACurrentMediaTime(); |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; |
| | | |
| | | UIView *textFieldView = _textFieldView; |
| | | |
| | | //Removing gesture recognizer (Enhancement ID: #14) |
| | | [textFieldView.window removeGestureRecognizer:_resignFirstResponderGesture]; |
| | | |
| | | // if ([textFieldView isKindOfClass:[UITextField class]]) |
| | | // { |
| | | // [(UITextField*)textFieldView removeTarget:self action:@selector(editingDidEndOnExit:) forControlEvents:UIControlEventEditingDidEndOnExit]; |
| | | // } |
| | | |
| | | // We check if there's a change in original frame or not. |
| | | if(_isTextViewContentInsetChanged == YES && |
| | | [textFieldView isKindOfClass:[UITextView class]]) |
| | | { |
| | | UITextView *textView = (UITextView*)textFieldView; |
| | | |
| | | __weak typeof(self) weakSelf = self; |
| | | |
| | | [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ |
| | | |
| | | __strong typeof(self) strongSelf = weakSelf; |
| | | |
| | | strongSelf.isTextViewContentInsetChanged = NO; |
| | | |
| | | [self showLog:[NSString stringWithFormat:@"Restoring %@ textView.contentInset to : %@",[strongSelf.textFieldView _IQDescription],NSStringFromUIEdgeInsets(strongSelf.startingTextViewContentInsets)]]; |
| | | |
| | | //Setting textField to it's initial contentInset |
| | | textView.contentInset = strongSelf.startingTextViewContentInsets; |
| | | textView.scrollIndicatorInsets = strongSelf.startingTextViewScrollIndicatorInsets; |
| | | |
| | | } completion:NULL]; |
| | | } |
| | | |
| | | //Setting object to nil |
| | | _textFieldView = nil; |
| | | |
| | | CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; |
| | | } |
| | | |
| | | //-(void)editingDidEndOnExit:(UITextField*)textField |
| | | //{ |
| | | // [self showLog:[NSString stringWithFormat:@"ReturnKey %@",NSStringFromSelector(_cmd)]]; |
| | | //} |
| | | |
| | | #pragma mark - UIStatusBar 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 |
| | | { |
| | | CFTimeInterval startTime = CACurrentMediaTime(); |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]]; |
| | | |
| | | //If textViewContentInsetChanged is changed then restore it. |
| | | if (_isTextViewContentInsetChanged == YES && |
| | | [_textFieldView isKindOfClass:[UITextView class]]) |
| | | { |
| | | UITextView *textView = (UITextView*)_textFieldView; |
| | | |
| | | __weak typeof(self) weakSelf = self; |
| | | |
| | | //Due to orientation callback we need to set it's original position. |
| | | [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{ |
| | | |
| | | __strong typeof(self) strongSelf = weakSelf; |
| | | |
| | | strongSelf.isTextViewContentInsetChanged = NO; |
| | | |
| | | [self showLog:[NSString stringWithFormat:@"Restoring %@ textView.contentInset to : %@",[strongSelf.textFieldView _IQDescription],NSStringFromUIEdgeInsets(strongSelf.startingTextViewContentInsets)]]; |
| | | |
| | | //Setting textField to it's initial contentInset |
| | | textView.contentInset = strongSelf.startingTextViewContentInsets; |
| | | textView.scrollIndicatorInsets = strongSelf.startingTextViewScrollIndicatorInsets; |
| | | } completion:NULL]; |
| | | } |
| | | |
| | | [self restorePosition]; |
| | | |
| | | CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime; |
| | | [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]]; |
| | | } |
| | | |
| | | #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) |
| | | for (Class aClass in self.touchResignedGestureIgnoreClasses) |
| | | { |
| | | if ([[touch view] isKindOfClass:aClass]) |
| | | { |
| | | return NO; |
| | | } |
| | | } |
| | | |
| | | return YES; |
| | | } |
| | | |
| | | /** Resigning textField. */ |
| | | - (BOOL)resignFirstResponder |
| | | { |
| | | UIView *textFieldView = _textFieldView; |
| | | |
| | | 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]; |
| | | |
| | | [self showLog:[NSString stringWithFormat:@"Refuses to Resign first responder: %@",[textFieldView _IQDescription]]]; |
| | | } |
| | | |
| | | return isResignFirstResponder; |
| | | } |
| | | else |
| | | { |
| | | return NO; |
| | | } |
| | | } |
| | | |
| | | /** Returns YES if can navigate to previous responder textField/textView, otherwise NO. */ |
| | | -(BOOL)canGoPrevious |
| | | { |
| | | //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 can becomeFirstResponder. |
| | | if (index != NSNotFound && |
| | | index > 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 |