IQKeyboardManager解析

我们在使用第三方框架时,往往需要import然后添加代码去实例化才能使用。但是IQKeyboardManager不需要任何代码就能自动解决键盘遮挡输入源,而且也提供了众多接口去实现各种需求。

当然我们可以选择在某些VC中代码中禁用:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [[IQKeyboardManager sharedManager] setEnable:NO];
}
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [[IQKeyboardManager sharedManager] setEnable:YES];
}

更统一优雅的做法禁用对应的类:

    [[IQKeyboardManager sharedManager].disabledDistanceHandlingClasses addObject:[XXX class]];

1. 工程结构

  1. IQKeyboardManager 核心管理类,所有监听操作都在此进行
  2. IQKeyboardReturnKeyHandler 管理next/done等UI操作的处理事件
  3. Categories
    • IQNSArray+Sort UIView.subviews排序的分类
    • IQUIScrollView+Additions 为UIScrollView添加属性
    • IQUITextFieldView+Additions 用于管理UITextField/UITextView的UIView分类
    • IQUIView+Hierarchy UIView层级结构分类
    • IQUIViewController+Additions vc添加NSLayoutConstraint的分类
    • IQUIWindow+Hierarchy UIWindow层级的分类
  4. Constants
    • IQKeyboardManagerConstants 枚举类型
    • IQKeyboardManagerConstantsInternal IQLayoutGuidePosition枚举
  5. IQTextView
    • IQTextView 实现UITextView支持placeholder的子类
  6. IQToolbar
    • IQBarButtonItem 为IQToolbar使用item实现的UIBarButtonItem子类
    • IQPreviousNextView IQPreviousNextView,复杂层级的情况下可以使用
    • IQTitleBarButtonItem 带title的BarButtonItem
    • IQToolbar UIToolbar的子类IQToolbar,即带3个按钮的toolbar
    • IQUIView+IQKeyboardToolbar 在UIKeyboard添加IQToolbar的uiview分类

2. 实现流程

实际上,不需要我们去添加代码就能使用是重写了+(void)load方法。

//在程序启动之前会按(父类->子类->类别)的顺序调用所有的类的 +load 方法
+(void)load
{
    //Enabling IQKeyboardManager. Loading asynchronous on main thread
    [self performSelectorOnMainThread:@selector(sharedManager) withObject:nil waitUntilDone:NO];
}

紧接着实现了一个IQKeyboardManager的单例。同时对外禁用了init和new方法的创建方式。

/**
 Unavailable. Please use sharedManager method
 */
-(nonnull instancetype)init NS_UNAVAILABLE;

/**
 Unavailable. Please use sharedManager method
 */
+ (nonnull instancetype)new NS_UNAVAILABLE;
/*  Singleton Object Initialization. */
-(instancetype)init
{
    if (self = [super init])
    {
        __weak typeof(self) weakSelf = self;
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //为了防止多线程环境下self对象可能被释放的情况
            __strong typeof(self) strongSelf = weakSelf;
           //创建一个NSMutableSet用以装载UITextField和UITextView的类及子类
            strongSelf.registeredClasses = [[NSMutableSet alloc] init];
            //注册通知,包括4个键盘通知(UIKeyboardWillShowNotification,UIKeyboardDidShowNotification,UIKeyboardWillHideNotification,UIKeyboardDidHideNotification) 和 [UITextField class]以及[UITextView class] 的通知(UITextFieldTextDidBeginEditingNotification,UITextFieldTextDidEndEditingNotification;UITextViewTextDidBeginEditingNotification,UITextViewTextDidEndEditingNotification)
            //以及方向改变(UIApplicationWillChangeStatusBarOrientationNotification)和状态栏frame改变(UIApplicationDidChangeStatusBarFrameNotification)的通知
            [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];
            //根据属性值判断是否有效,默认是无效的,需要设置[IQKeyboardManager sharedManager].shouldResignOnTouchOutside = YES;(大神编程的思考方式值得我们学习)
            strongSelf.resignFirstResponderGesture.enabled = strongSelf.shouldResignOnTouchOutside;

            //Setting it's initial values
            strongSelf.animationDuration = 0.25;
            strongSelf.animationCurve = UIViewAnimationCurveEaseInOut;
            //开启键盘不遮挡功能
            [self setEnable:YES];
            //设置text field到键盘的距离为10
           [self setKeyboardDistanceFromTextField:10.0];
            //当为yes时,点击next/previous/done 发出输入声响
            [self setShouldPlayInputClicks:YES];
            //设置点击其他地方收键盘无效
            [self setShouldResignOnTouchOutside:NO];
            //设置不为所以textField/textView设置keyboardAppearance,并且统一使用默认风格UIKeyboardAppearanceDefault
            [self setOverrideKeyboardAppearance:NO];
            [self setKeyboardAppearance:UIKeyboardAppearanceDefault];
            //显示toolbar
            [self setEnableAutoToolbar:YES];
            //设置防止键盘管理器将rootView向上滑到键盘高度以上
            [self setPreventShowingBottomBlankSpace:YES];
            //设置在IQToolbar中添加textField占位符
            [self setShouldShowToolbarPlaceholder:YES];
            //设置根据Textfield的创建子视图的先后顺序,创建工具栏。详细参考responderViews,有3种模式:IQAutoToolbarBySubviews,IQAutoToolbarByTag,IQAutoToolbarByPosition
            [self setToolbarManageBehaviour:IQAutoToolbarBySubviews];
            //如果设置为YES,会在viewController's view每次改变frame时调用'setNeedsLayout' and 'layoutIfNeeded'
            [self setLayoutIfNeededOnUpdate:NO];
            // If YES, then always consider UINavigationController.view begin point as {0,0}, this is a workaround to fix a bug #464 because there are no notification mechanism exist when UINavigationController.view.frame gets changed internally.
            [self setShouldFixInteractivePopGestureRecognizer:YES];
            
            //Loading IQToolbar, IQTitleBarButtonItem, IQBarButtonItem to fix first time keyboard appearance delay (Bug ID: #550)
            {
                UITextField *view = [[UITextField alloc] init];
                //添加done按钮
                [view addDoneOnKeyboardWithTarget:nil action:nil];
                //设置toolbar的next/previous/done
                [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];
            //如果为YES,使用textField的tintColor属性为IQToolbar的tintColor;为NO,IQToolbar的tintColor为黑色
            [self setShouldToolbarUsesTextFieldTintColor:NO];
        });
    }
    return self;
}

接下来我们打开[[IQKeyboardManager sharedManager] setEnableDebugging:YES];来看看他的调用流程。

  1. 当弹出键盘时,会收到通知调用该方法:
#pragma mark - UITextFieldView Delegate methods
/**  UITextFieldTextDidBeginEditingNotification, UITextViewTextDidBeginEditingNotification. Fetching UITextFieldView object. */
-(void)textFieldViewDidBeginEditing:(NSNotification*)notification
{
    //获取当前CoreAnimation转化为秒的绝对时间
    CFTimeInterval startTime = CACurrentMediaTime();
    //使用_cmd打印当前方法名
    [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]];

    //  获取到该textfield并保存便于后续使用
    _textFieldView = notification.object;
    
    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
        {
            //如果需要在textFields和它的同类中添加,则添加工具栏
            //该方法会把层级中的textField和textview,IQDropDownTextField,IQTextView取出根据不同情况添加toolbar
            [self addToolbarIfRequired];
        }
    }
    else
    {
        //如果不添加则移除IQToolbar
        [self removeToolbarIfRequired];
    }
    
    //Adding Geture recognizer to window    (Enhancement ID: #14)
    [_resignFirstResponderGesture setEnabled:[self privateShouldResignOnTouchOutside]];
    [_textFieldView.window addGestureRecognizer:_resignFirstResponderGesture];

    if ([self privateIsEnabled] == YES)
    {
        if (CGRectEqualToRect(_topViewBeginRect, CGRectZero))    //  (Bug ID: #5)
        {
            //  keyboard is not showing(At the beginning only). We should save rootViewRect and _layoutGuideConstraintInitialConstant.
            _layoutGuideConstraint = [[_textFieldView viewController] IQLayoutGuideConstraint];
            _layoutGuideConstraintInitialConstant = [_layoutGuideConstraint constant];
            //获取到顶层vc
            _rootViewController = [_textFieldView topMostController];
            if (_rootViewController == nil)  _rootViewController = [[self keyWindow] topMostWindowController];
            
            _topViewBeginRect = _rootViewController.view.frame;
            
#ifdef __IPHONE_11_0
            if (@available(iOS 11.0, *)) {
                self.initialAdditionalSafeAreaInsets = _rootViewController.additionalSafeAreaInsets;
            }
#endif

            if (_topViewBeginRect.origin.y != 0 &&
                _shouldFixInteractivePopGestureRecognizer &&
                [_rootViewController isKindOfClass:[UINavigationController class]] &&
                [_rootViewController modalPresentationStyle] != UIModalPresentationFormSheet &&
                [_rootViewController modalPresentationStyle] != UIModalPresentationPageSheet)
            {
                UIWindow *window = [self keyWindow];
                if (window)
                {
                    _topViewBeginRect.origin.y = window.frame.size.height-_rootViewController.view.frame.size.height;
                }
                else
                {
                    _topViewBeginRect.origin.y = 0;
                }
            }
            
            [self showLog:[NSString stringWithFormat:@"Saving %@ beginning Frame: %@",[_rootViewController _IQDescription], NSStringFromCGRect(_topViewBeginRect)]];
        }
        
        //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 != nil  &&
            [_textFieldView isAlertViewTextField] == NO)
        {
            //  keyboard is already showing. adjust frame.
            /** 1.首先获取KeyWindow
                2.然后根据_textFieldView获取RootViewController
                3.将_textFieldView.frame转换到keyWindow的坐标中
                4.添加上keyboardDistanceFromTextField高度
                5.根据多种情况重新设置_textFieldView所在vc的view的frame
                如下,我们进入详细看看
            **/
            [self adjustFrame];
        }
    }
    
//    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]];
}


/* Adjusting RootViewController's frame according to interface orientation. */
-(void)adjustFrame
{
    //  We are unable to get textField object while keyboard showing on UIWebView's textField.  (Bug ID: #11)
    //作者提到了,如果是UIWebView中的textField,我们是获取不到的
    if (_textFieldView == nil)   return;
    
    CFTimeInterval startTime = CACurrentMediaTime();
    [self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]];

    //  获取keyWindow.
    UIWindow *keyWindow = [self keyWindow];
    
    //  获取_textFieldView所在的vc,如果不存在则获取keyWindow的 topController.  (Bug ID: #1, #4)
    UIViewController *rootController = [_textFieldView topMostController];
    if (rootController == nil)  rootController = [keyWindow topMostWindowController];
    
    //  将_textFieldView的frame转换到keyWindow的坐标系中.
    CGRect textFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];
    //  Getting RootViewRect.
    CGRect rootViewRect = [[rootController view] frame];
    //Getting statusBarFrame

    //Maintain keyboardDistanceFromTextField textfield到键盘的距离,默认为10
    CGFloat specialKeyboardDistanceFromTextField = _textFieldView.keyboardDistanceFromTextField;
    
    if (_textFieldView.isSearchBarTextField)
    {
      //如果包含UISearchBar
        UISearchBar *searchBar = (UISearchBar*)[_textFieldView superviewOfClassType:[UISearchBar class]];
        specialKeyboardDistanceFromTextField = searchBar.keyboardDistanceFromTextField;
    }
    
    CGFloat keyboardDistanceFromTextField = (specialKeyboardDistanceFromTextField == kIQUseDefaultKeyboardDistance)?_keyboardDistanceFromTextField:specialKeyboardDistanceFromTextField;
    CGSize kbSize = _kbSize;
    kbSize.height += keyboardDistanceFromTextField;

    CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
    
    //  (Bug ID: #250)
    IQLayoutGuidePosition layoutGuidePosition = IQLayoutGuidePositionNone;
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
    //If topLayoutGuide constraint
    if (_layoutGuideConstraint && (_layoutGuideConstraint.firstItem == [[_textFieldView viewController] topLayoutGuide] ||
        _layoutGuideConstraint.secondItem == [[_textFieldView viewController] topLayoutGuide]))
    {
        layoutGuidePosition = IQLayoutGuidePositionTop;
    }
    //If bottomLayoutGuice constraint
    else if (_layoutGuideConstraint && (_layoutGuideConstraint.firstItem == [[_textFieldView viewController] bottomLayoutGuide] ||
             _layoutGuideConstraint.secondItem == [[_textFieldView viewController] bottomLayoutGuide]))
    {
        layoutGuidePosition = IQLayoutGuidePositionBottom;
    }
#pragma clang diagnostic pop

    CGFloat topLayoutGuide = CGRectGetHeight(statusBarFrame);

    CGFloat move = 0;
    //move 大于0 隐藏; 小于0,显示
    //  +Move positive = textField is hidden.
    //  -Move negative = textField is showing.
    
    //  Checking if there is bottomLayoutGuide attached (Bug ID: #250)
    if (layoutGuidePosition == IQLayoutGuidePositionBottom)
    {
        //  Calculating move position.
        move = CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-kbSize.height);
    }
    else
    {
        //  Calculating move position. Common for both normal and special cases.
        move = MIN(CGRectGetMinY(textFieldViewRect)-(topLayoutGuide+5), CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-kbSize.height));
    }
    
    [self showLog:[NSString stringWithFormat:@"Need to move: %.2f",move]];

    UIScrollView *superScrollView = nil;
    //查找当前输入框的父视图是否有ScrollView
    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
        {
            //如果不能,则再取他的父视图,并且也是ScrollView才行
            //  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;

                [strongSelf.lastScrollView setContentInset:strongSelf.startingContentInsets];
                strongSelf.lastScrollView.scrollIndicatorInsets = strongSelf.startingScrollIndicatorInsets;
            } completion:NULL];
            
            if (_lastScrollView.shouldRestoreScrollViewContentOffset)
            {
                [_lastScrollView setContentOffset:_startingContentOffset animated:YES];
            }

            _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;

                [strongSelf.lastScrollView setContentInset:strongSelf.startingContentInsets];
                strongSelf.lastScrollView.scrollIndicatorInsets = strongSelf.startingScrollIndicatorInsets;
            } completion:NULL];

            if (_lastScrollView.shouldRestoreScrollViewContentOffset)
            {
                [_lastScrollView setContentOffset:_startingContentOffset animated:YES];
            }
            
            _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;
            UIScrollView *superScrollView = _lastScrollView;

            //Looping in upper hierarchy until we don't found any scrollView in it's upper hirarchy till UIWindow object.
            while (superScrollView &&
                   (move>0?(move > (-superScrollView.contentOffset.y-superScrollView.contentInset.top)):superScrollView.contentOffset.y>0) )
            {
                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/*-5*/);   //-5 is for good UI.//Commenting -5 (Bug ID: #69)
                
                //[_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 hierrchy.)
                //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))
                {
                    CGFloat maintainTopLayout = 0;
                    
                    //When uncommenting this, each calculation goes to well, but don't know why scrollView doesn't adjusting it's contentOffset at bottom
//                    if ([_textFieldView.viewController respondsToSelector:@selector(topLayoutGuide)])
//                        maintainTopLayout = [_textFieldView.viewController.topLayoutGuide length];
//                    else
                        maintainTopLayout = CGRectGetMaxY(_textFieldView.viewController.navigationController.navigationBar.frame);

                    maintainTopLayout+= 10; //For good UI
                    
                    //  Converting Rectangle according to window bounds.
                    CGRect currentTextFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];
                    
                    //Calculating expected fix distance which needs to be managed from navigation bar
                    CGFloat expectedFixDistance = CGRectGetMinY(currentTextFieldViewRect) - maintainTopLayout;
                    
                    //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;

                    strongSelf.lastScrollView.contentInset = movedInsets;
                    
                    UIEdgeInsets newInset = strongSelf.lastScrollView.scrollIndicatorInsets;
                    newInset.bottom = movedInsets.bottom;
                    strongSelf.lastScrollView.scrollIndicatorInsets = newInset;

                } completion:NULL];

                [self showLog:[NSString stringWithFormat:@"%@ new ContentInset : %@",[_lastScrollView _IQDescription], NSStringFromUIEdgeInsets(_lastScrollView.contentInset)]];
            }
        }
        //Going ahead. No else if.
    }
    
    if (layoutGuidePosition == IQLayoutGuidePositionTop)
    {
        CGFloat constant = MIN(_layoutGuideConstraintInitialConstant, _layoutGuideConstraint.constant-move);
        
        __weak typeof(self) weakSelf = self;

        [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{

            __strong typeof(self) strongSelf = weakSelf;

            weakSelf.layoutGuideConstraint.constant = constant;
            [strongSelf.rootViewController.view setNeedsLayout];
            [strongSelf.rootViewController.view layoutIfNeeded];
        } completion:NULL];
    }
    //If bottomLayoutGuice constraint
    else if (layoutGuidePosition == IQLayoutGuidePositionBottom)
    {
        CGFloat constant = MAX(_layoutGuideConstraintInitialConstant, _layoutGuideConstraint.constant+move);
        
        __weak typeof(self) weakSelf = self;

        [UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{

            __strong typeof(self) strongSelf = weakSelf;

            weakSelf.layoutGuideConstraint.constant = constant;
            [strongSelf.rootViewController.view setNeedsLayout];
            [strongSelf.rootViewController.view layoutIfNeeded];
        } completion:NULL];
    }
    //If not constraint
    else
    {
        //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 textViewHeight = MIN(CGRectGetHeight(_textFieldView.frame), (CGRectGetHeight(keyWindow.frame)-kbSize.height-(topLayoutGuide)));
            
            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;
                    
                    [self showLog:[NSString stringWithFormat:@"%@ Old UITextView.contentInset : %@",[strongSelf.textFieldView _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 = strongSelf.textFieldView.frame.size.height-textViewHeight;
                    textView.contentInset = newContentInset;
                    textView.scrollIndicatorInsets = newContentInset;
                    strongSelf.isTextViewContentInsetChanged = YES;
                    
                    [self showLog:[NSString stringWithFormat:@"%@ New UITextView.contentInset : %@",[strongSelf.textFieldView _IQDescription], NSStringFromUIEdgeInsets(textView.contentInset)]];
                    
                } completion:NULL];
            }
        }

        //  Special case for iPad modalPresentationStyle.
        if ([rootController modalPresentationStyle] == UIModalPresentationFormSheet ||
            [rootController modalPresentationStyle] == UIModalPresentationPageSheet)
        {
            [self showLog:[NSString stringWithFormat:@"Found Special case for Model Presentation Style: %ld",(long)(rootController.modalPresentationStyle)]];
            
            //  +Positive or zero.
            if (move>=0)
            {
                // We should only manipulate y.
                rootViewRect.origin.y -= move;
                
                //  From now prevent keyboard manager to slide up the rootView to more than keyboard height. (Bug ID: #93)
                if (_preventShowingBottomBlankSpace == YES)
                {
                    CGFloat minimumY = (CGRectGetHeight(keyWindow.frame)-rootViewRect.size.height-topLayoutGuide)/2-(kbSize.height-keyboardDistanceFromTextField);
                    
                    rootViewRect.origin.y = MAX(rootViewRect.origin.y, minimumY);
                }
                
                [self showLog:@"Moving Upward"];
                //  Setting adjusted rootViewRect
                [self setRootViewFrame:rootViewRect];
                _movedDistance = (_topViewBeginRect.origin.y-rootViewRect.origin.y);
            }
            //  -Negative
            else
            {
                //  Calculating disturbed distance. Pull Request #3
                CGFloat disturbDistance = CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect);
                
                //  disturbDistance Negative = frame disturbed.
                //  disturbDistance positive = frame not disturbed.
                if(disturbDistance<=0)
                {
                    // We should only manipulate y.
                    rootViewRect.origin.y -= MAX(move, disturbDistance);
                    
                    [self showLog:@"Moving Downward"];
                    //  Setting adjusted rootViewRect
                    [self setRootViewFrame:rootViewRect];
                    _movedDistance = (_topViewBeginRect.origin.y-rootViewRect.origin.y);
                }
            }
        }
        //If presentation style is neither UIModalPresentationFormSheet nor UIModalPresentationPageSheet then going ahead.(General case)
        else
        {
            //  +Positive or zero.
            if (move>=0)
            {
                rootViewRect.origin.y -= move;
                
                //  From now prevent keyboard manager to slide up the rootView to more than keyboard height. (Bug ID: #93)
                if (_preventShowingBottomBlankSpace == YES)
                {
                    rootViewRect.origin.y = MAX(rootViewRect.origin.y, MIN(0, -kbSize.height+keyboardDistanceFromTextField));
                }
                
                [self showLog:@"Moving Upward"];
                //  Setting adjusted rootViewRect
                [self setRootViewFrame:rootViewRect];
                _movedDistance = (_topViewBeginRect.origin.y-rootViewRect.origin.y);
            }
            //  -Negative
            else
            {
                CGFloat disturbDistance = CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect);
                
                //  disturbDistance Negative = frame disturbed. Pull Request #3
                //  disturbDistance positive = frame not disturbed.
                if(disturbDistance<=0)
                {
                    rootViewRect.origin.y -= MAX(move, disturbDistance);
                    
                    [self showLog:@"Moving Downward"];
                    //  Setting adjusted rootViewRect
                    //设置 [controller.view setFrame:frame];
                    [self setRootViewFrame:rootViewRect];
                    _movedDistance = (_topViewBeginRect.origin.y-rootViewRect.origin.y);
                }
            }
        }
    }
    
    CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime;
    [self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]];
}

  1. 当从一个textview/textfield切换到另一个textview/textfield。

    • 手动点击切换:

      #pragma mark - UITextFieldView Delegate methods
      /**  UITextFieldTextDidBeginEditingNotification, UITextViewTextDidBeginEditingNotification. Fetching UITextFieldView object. */
      -(void)textFieldViewDidBeginEditing:(NSNotification*)notification
      {
         /**
         1.如果给textField/textView重写Appearance,_overrideKeyboardAppearance,则设置
         2.如果需要添加toolbar,则添加,并加上对应的action
         3.添加_resignFirstResponderGesture手势到_textFieldView.window;并判断privateIsEnabled是否允许以及响应区域
         4.keyboard显示之后,调整vc的frame
         
         **/
      }
      

    • 如果是点击ToolBar跳转:

      //该方法添加对应的action    
      [textField addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:) shouldShowPlaceholder:_shouldShowToolbarPlaceholder];
      

      以previousAction为例。

      -(void)previousAction:(IQBarButtonItem*)barButton
      {
         //如果需要播放声音,则播放
          if (_shouldPlayInputClicks)
          {
              [[UIDevice currentDevice] playInputClick];
          }
         //拿到所以可以响应的view,判断当前textField如果不是第一个,则可以此操作,否则返回NO
          if ([self canGoPrevious])
          {
              UIView *currentTextFieldView = _textFieldView;
             //拿到响应的textview数组,取出当前的上一个,并判断是否能够成为第一响应者
             //成为第一响应者同时,会监听到textFieldViewDidBeginEditing 和keyboardWillShow通知
              BOOL isAcceptAsFirstResponder = [self goPrevious];
              
              if (isAcceptAsFirstResponder == YES && barButton.invocation)
              {
                 //当有多个参数调用时,会使用到invocation;系统的NSObject提供的performSelector:withObject的方法只提供了最多两个参数的调用
                 //NSInvocation 是命令模式的一种实现,它包含选择器、方法签名、相应的参数以及目标对象
                  if (barButton.invocation.methodSignature.numberOfArguments > 2)
                  {
                      [barButton.invocation setArgument:¤tTextFieldView atIndex:2];
                  }
      
                  [barButton.invocation invoke];
              }
          }
      }
      

  2. 最后是收键盘,调用实现的resignFirstResponder方法,并且监听keyboardWillHide以及keyboardDidHide。

    /** Resigning textField. */
    - (BOOL)resignFirstResponder
    {
        if (_textFieldView)
        {
            //  Retaining textFieldView
            UIView *textFieldRetain = _textFieldView;
            
            //Resigning first responder
            BOOL isResignFirstResponder = [_textFieldView resignFirstResponder];
            
            //  If it refuses then becoming it as first responder again.    (Bug ID: #96)
            if (isResignFirstResponder == NO)
            {
                //If it refuses to resign then becoming it first responder again for getting notifications callback.
                [textFieldRetain becomeFirstResponder];
                
                [self showLog:[NSString stringWithFormat:@"Refuses to Resign first responder: %@",[_textFieldView _IQDescription]]];
            }
            
            return isResignFirstResponder;
        }
        else
        {
            return NO;
        }
    }
    
    //在该方法中将rootViewController还原为原来的frame
    - (void)keyboardWillHide:(NSNotification*)aNotification
    
    //在该方法中将用于保存rootViewController.view.frame的_topViewBeginRect置为CGRectZero
    - (void)keyboardDidHide:(NSNotification*)aNotification
    

    至此就结束了。

3. 总结

大体流程如下图:

IQKeyboardManagerFlowDiagram.jpg
  1. 首先从+(void)load方法加载单例开始。

  2. 然后分别注册UIKeyboard,UITextview,UITextfield,Orientation通知。

  3. UIKeyboardWillShow:首先保存noti到 kbShowNotification;然后判断KeyboardManager是否enable,NO则结束;YES则获取keyboard通知属性,判断是否Default动画,NO则保存AnimationCurve到 animationCurve;保存 oldSize of kbsize到oldkbSize,保存keyboardSize到_kbSize;判断kbSize和oldKBSize是否相等,相等则调整Frame。

  4. adjust frame 首先会判断当前_textFieldView是否为空,因为如果为webview是获取不到textfield的。不为空则设置 _iskeyboardShowing 为YES;获取KeyWindow,获取window上的TopMostController保存为rootcontroller;一顿操作获取到move的值;然后判断当前textfield是否有superScorllview,如果找到则调整superScorllview的contentOffset;没有找到则通过Move值判断,如果大于0,则setRootViewFrame向下;如果小于等于0,则RootVC向上偏移。

  5. UITextFieldTextDidBeginEditingNotification/UITextViewTextDidBeginEditingNotification:则是添加UIToolbar作为inputAccessoryView。首先会将当前textview保存到_textFieldView;然后判断几个属性做相应的设置,比如KeyboardAppearance,是否添加收键盘手势,是否允许添加toolbar,当然这里允许才会继续往下。然后会执行AddToolbarIfRequired;他先是会去找到所有的Textfield/textView,然后根据不同的策略排序;然后判断是否有InputAccessoryView,没有则创建;有的话则根据当前的是第一个还是最后一个去禁用对应的Next/Previous。最后判断 _kbSize, oldKBSize是否相等,不相同则调用adjustFrame刷新。

  6. UIApplicationWillChangeStatusBarOrientationNotification: 如果textViewContentInsetChanged为NO则结束;YES则设置设置 textFieldView.frame为_textFieldViewIntialFrame。

  7. UIKeyboardWillHide: 分别保存_textFieldFrame到TextFieldViewIntialFrame;将lastScorllview置为nil,kbSize置为CGSizeZero,并将rootViewController的frame还原。

详细逻辑可以参考下图:

IQKeyboardManagerCFD.jpg

你可能感兴趣的:(IQKeyboardManager解析)