來源:Draveness (https://github.com/Draveness)
鏈接:http://www.itdecent.cn/p/8bce6278cb9d
這篇文章會(huì)對(duì) IQKeyboardManager 自動(dòng)解決鍵盤遮擋問題的方法進(jìn)行分析。
最近在項(xiàng)目中使用了IQKeyboardManager來解決UITextField被鍵盤遮擋的問題,這個(gè)框架的使用方法可以說精簡到了極致,只需要將IQKeyboardManager加入Podfile,然后pod install就可以了。
pod'IQKeyboardManager'
這篇文章的題目《零行代碼解決鍵盤遮擋問題》來自于開源框架的介紹:
Codeless drop-in universal library allows to prevent
issues of keyboard sliding up and cover UITextField/UITextView. Neither
need to write any code nor any setup required and much more.
因?yàn)樵陧?xiàng)目中使用了 IQKeyboardManager,所以,我想通過閱讀其源代碼來了解這個(gè)黑箱是如何工作的。
雖然這個(gè)框架的實(shí)現(xiàn)的方法是比較簡單的,不過它的實(shí)現(xiàn)代碼不是很容易閱讀,框架因?yàn)榘撕芏嗯c UI 有關(guān)的實(shí)現(xiàn)細(xì)節(jié),所以代碼比較復(fù)雜。
架構(gòu)分析
說是架構(gòu)分析,其實(shí)只是對(duì)IQKeyboardManager中包含的類以及文件有一個(gè)粗略地了解,研究一下這個(gè)項(xiàng)目的層級(jí)是什么樣的。

IQKeyboardManager-Hierarchy
整個(gè)項(xiàng)目中最核心的部分就是IQKeyboardManager這個(gè)類,它負(fù)責(zé)管理鍵盤出現(xiàn)或者隱藏時(shí)視圖移動(dòng)的距離,是整個(gè)框架中最核心的部分。
在這個(gè)框架中還有一些用于支持 IQKeyboardManager 的分類,以及顯示在鍵盤上面的 IQToolBar:

IQToolBa
使用紅色標(biāo)記的部分就是IQToolBar,左側(cè)的按鈕可以在不同的UITextField之間切換,中間的文字是UITextField.placeholderText,右邊的Done應(yīng)該就不需要解釋了。
這篇文章會(huì)主要分析IQKeyboardManager中解決的問題,會(huì)用小篇幅介紹包含占位符(Placeholder)IQTextView的實(shí)現(xiàn)。
IQTextView 的實(shí)現(xiàn)
在具體研究如何解決鍵盤遮擋問題之前,我們先分析一下框架中最簡單的一部分IQTextView是如何為UITextView添加占位符的。
@interfaceIQTextView:UITextView@end
IQTextView繼承自UITextView,它只是在UITextView上添加上了一個(gè)placeHolderLabel。
在初始化時(shí),我們會(huì)為UITextViewTextDidChangeNotification注冊通知:
- (void)initialize? {? ? [[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(refreshPlaceholder) name:UITextViewTextDidChangeNotificationobject:self];}
在每次 UITextView 中的 text 更改時(shí),就會(huì)調(diào)用refreshPlaceholder方法更新placeHolderLabel的alpha值來隱藏或者顯示 label:
-(void)refreshPlaceholder {if([[selftext] length]) {? ? ? ? [placeHolderLabel setAlpha:0];? ? }else{? ? ? ? [placeHolderLabel setAlpha:1];? ? }? ? [selfsetNeedsLayout];? ? [selflayoutIfNeeded];}
IQKeyboardManager
下面就會(huì)進(jìn)入這篇文章的正題:IQKeyboardManager。
如果你對(duì) iOS 開發(fā)比較熟悉,可能會(huì)發(fā)現(xiàn)每當(dāng)一個(gè)類的名字中包含了manager,那么這個(gè)類可能可能遵循單例模式,IQKeyboardManager也不例外。
IQKeyboardManager 的初始化
當(dāng)IQKeyboardManager初始化的時(shí)候,它做了這么幾件事情:
監(jiān)聽有關(guān)鍵盤的通知
[[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotificationobject:nil]; [[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotificationobject:nil]; [[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotificationobject:nil];
注冊與UITextField以及UITextView有關(guān)的通知
[selfregisterTextFieldViewClass:[UITextFieldclass]? didBeginEditingNotificationName:UITextFieldTextDidBeginEditingNotificationdidEndEditingNotificationName:UITextFieldTextDidEndEditingNotification]; [selfregisterTextFieldViewClass:[UITextViewclass]? didBeginEditingNotificationName:UITextViewTextDidBeginEditingNotificationdidEndEditingNotificationName:UITextViewTextDidEndEditingNotification];
調(diào)用的方法將通知綁定到了textFieldViewDidBeginEditing:和textFieldViewDidEndEditing:方法上
- (void)registerTextFieldViewClass:(nonnullClass)aClass? ? didBeginEditingNotificationName:(nonnullNSString*)didBeginEditingNotificationName? ? ? didEndEditingNotificationName:(nonnullNSString*)didEndEditingNotificationName {? ? ? [[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(textFieldViewDidBeginEditing:) name:didBeginEditingNotificationName object:nil];? ? ? [[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(textFieldViewDidEndEditing:) name:didEndEditingNotificationName object:nil];? }
初始化一個(gè)UITapGestureRecognizer,在點(diǎn)擊UITextField對(duì)應(yīng)的UIWindow的時(shí)候,收起鍵盤
strongSelf.tapGesture = [[UITapGestureRecognizeralloc] initWithTarget:selfaction:@selector(tapRecognized:)];
- (void)tapRecognized:(UITapGestureRecognizer*)gesture {if(gesture.state ==UIGestureRecognizerStateEnded)? ? ? ? [selfresignFirstResponder]; }
初始化一些默認(rèn)屬性,例如鍵盤距離、覆寫鍵盤的樣式等
strongSelf.animationDuration =0.25; strongSelf.animationCurve =UIViewAnimationCurveEaseInOut; [selfsetKeyboardDistanceFromTextField:10.0]; [selfsetShouldPlayInputClicks:YES]; [selfsetShouldResignOnTouchOutside:NO]; [selfsetOverrideKeyboardAppearance:NO]; [selfsetKeyboardAppearance:UIKeyboardAppearanceDefault]; [selfsetEnableAutoToolbar:YES]; [selfsetPreventShowingBottomBlankSpace:YES]; [selfsetShouldShowTextFieldPlaceholder:YES]; [selfsetToolbarManageBehaviour:IQAutoToolbarBySubviews]; [selfsetLayoutIfNeededOnUpdate:NO];
設(shè)置不需要解決鍵盤遮擋問題的類
strongSelf.disabledDistanceHandlingClasses = [[NSMutableSetalloc] initWithObjects:[UITableViewControllerclass],nil]; strongSelf.enabledDistanceHandlingClasses = [[NSMutableSetalloc] init]; strongSelf.disabledToolbarClasses = [[NSMutableSetalloc] init]; strongSelf.enabledToolbarClasses = [[NSMutableSetalloc] init]; strongSelf.toolbarPreviousNextAllowedClasses = [[NSMutableSetalloc] initWithObjects:[UITableViewclass],[UICollectionViewclass],[IQPreviousNextViewclass],nil]; strongSelf.disabledTouchResignedClasses = [[NSMutableSetalloc] init]; strongSelf.enabledTouchResignedClasses = [[NSMutableSetalloc] init];
整個(gè)初始化方法大約有幾十行的代碼,在這里就不再展示整個(gè)方法的全部代碼了。
基于通知的解決方案
在這里,我們以 UITextField 為例,分析方法的調(diào)用流程。
在初始化方法中,我們注冊了很多的通知,包括鍵盤的出現(xiàn)和隱藏,UITextField開始編輯與結(jié)束編輯。
UIKeyboardWillShowNotificationUIKeyboardWillHideNotificationUIKeyboardDidHideNotificationUITextFieldTextDidBeginEditingNotificationUITextFieldTextDidEndEditingNotification
在這些通知響應(yīng)時(shí),會(huì)執(zhí)行以下的方法:
NotificationSelector
UIKeyboardWillShowNotification@selector(keyboardWillShow:)
UIKeyboardWillHideNotification@selector(keyboardWillHide:)
UIKeyboardDidHideNotification@selector(keyboardDidHide:)
UITextFieldTextDidBeginEditingNotification@selector(textFieldViewDidBeginEditing:)
UITextFieldTextDidEndEditingNotification@selector(textFieldViewDidEndEditing:)
整個(gè)解決方案其實(shí)都是基于 iOS 中的通知系統(tǒng)的;在事件發(fā)生時(shí),調(diào)用對(duì)應(yīng)的方法做出響應(yīng)。
開啟 Debug 模式
在閱讀源代碼的過程中,我發(fā)現(xiàn)IQKeyboardManager提供了enableDebugging這一屬性,可以通過開啟它,來追蹤方法的調(diào)用,我們可以在 Demo 加入下面這行代碼:
[IQKeyboardManager sharedManager].enableDebugging =YES;
鍵盤的出現(xiàn)
然后運(yùn)行工程,在 Demo 中點(diǎn)擊一個(gè)UITextField

easiest-integration-demo
上面的操作會(huì)打印出如下所示的 Log:
IQKeyboardManager: ****** textFieldViewDidBeginEditing: started ******IQKeyboardManager: addingUIToolbarsifrequiredIQKeyboardManager: Saving beginning Frame: {{0,0}, {320,568}}IQKeyboardManager: ****** adjustFrame started ******IQKeyboardManager: Need to move:-451.00IQKeyboardManager: ****** adjustFrame ended ******IQKeyboardManager: ****** textFieldViewDidBeginEditing: ended ******IQKeyboardManager: ****** keyboardWillShow: started ******IQKeyboardManager: ****** adjustFrame started ******IQKeyboardManager: Need to move:-154.00IQKeyboardManager: ****** adjustFrame ended ******IQKeyboardManager: ****** keyboardWillShow: ended ******
我們可以通過分析- textFieldViewDidBeginEditing:以及- keyboardWillShow:方法來了解這個(gè)項(xiàng)目的原理。
textFieldViewDidBeginEditing:
當(dāng)UITextField被點(diǎn)擊時(shí),方法- textFieldViewDidBeginEditing:被調(diào)用,但是注意這里的方法并不是代理方法,它只是一個(gè)跟代理方法同名的方法,根據(jù) Log,它做了三件事情:
為UITextField添加IQToolBar
在調(diào)整 frame 前,保存當(dāng)前 frame,以備之后鍵盤隱藏后的恢復(fù)
調(diào)用- adjustFrame方法,將視圖移動(dòng)到合適的位置
添加 ToolBar
添加 ToolBar 是通過方法- addToolbarIfRequired實(shí)現(xiàn)的,在- textFieldViewDidBeginEditing:先通過- privateIsEnableAutoToolbar判斷 ToolBar 是否需要添加,再使用相應(yīng)方法- addToolbarIfRequired實(shí)現(xiàn)這一目的。
這個(gè)方法會(huì)根據(jù)根視圖上UITextField的數(shù)量執(zhí)行對(duì)應(yīng)的代碼,下面為一般情況下執(zhí)行的代碼:
- (void)addToolbarIfRequired {NSArray*siblings = [selfresponderViews];for(UITextField*textFieldinsiblings) {? ? ? ? [textField addPreviousNextDoneOnKeyboardWithTarget:selfpreviousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:) shouldShowPlaceholder:_shouldShowTextFieldPlaceholder];? ? ? ? textField.inputAccessoryView.tag = kIQPreviousNextButtonToolbarTag;? ? ? ? IQToolbar *toolbar = (IQToolbar*)[textField inputAccessoryView];? ? ? ? toolbar.tintColor = [UIColorblackColor];? ? ? ? [toolbar setTitle:textField.drawingPlaceholderText];? ? ? ? [textField setEnablePrevious:NOnext:YES];? ? }}
在鍵盤上的IQToolBar一般由三部分組成:
切換UITextField的箭頭按鈕
指示當(dāng)前UITextField的 placeholder
Done Button

IQToolBarIte
這些 item 都是IQBarButtonItem的子類
這些IQBarButtonItem以及IQToolBar都是通過方法- addPreviousNextDoneOnKeyboardWithTarget:previousAction:nextAction:doneAction:或者類似方法添加的:
- (void)addPreviousNextDoneOnKeyboardWithTarget:(id)target previousAction:(SEL)previousAction nextAction:(SEL)nextAction doneAction:(SEL)doneAction titleText:(NSString*)titleText {? ? IQBarButtonItem *prev = [[IQBarButtonItem alloc] initWithImage:imageLeftArrow style:UIBarButtonItemStylePlaintarget:target action:previousAction];? ? IQBarButtonItem *next = [[IQBarButtonItem alloc] initWithImage:imageRightArrow style:UIBarButtonItemStylePlaintarget:target action:nextAction];? ? IQTitleBarButtonItem *title = [[IQTitleBarButtonItem alloc] initWithTitle:self.shouldHideTitle?nil:titleText];? ? IQBarButtonItem *doneButton =[[IQBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDonetarget:target action:doneAction];? ? IQToolbar *toolbar = [[IQToolbar alloc] init];? ? toolbar.barStyle =UIBarStyleDefault;? ? toolbar.items = @[prev, next, title, doneButton];? ? toolbar.titleInvocation =self.titleInvocation;? ? [(UITextField*)selfsetInputAccessoryView:toolbar];}
上面是方法簡化后的實(shí)現(xiàn)代碼,初始化需要的IQBarButtonItem,然后將這些IQBarButtonItem全部加入到IQToolBar上,最后設(shè)置UITextField的accessoryView。
保存 frame
這一步的主要目的是為了在鍵盤隱藏時(shí)恢復(fù)到原來的狀態(tài),其實(shí)現(xiàn)也非常簡單:
_rootViewController = [_textFieldView topMostController];
_topViewBeginRect = _rootViewController.view.frame;
獲取topMostController,在_topViewBeginRect中保存frame。
adjustFrame
在上述的任務(wù)都完成之后,最后就需要調(diào)用- adjustFrame方法來調(diào)整當(dāng)前根試圖控制器的frame了:
我們只會(huì)研究一般情況下的實(shí)現(xiàn)代碼,因?yàn)檫@個(gè)方法大約有 400 行代碼對(duì)不同情況下的實(shí)現(xiàn)有不同的路徑,包括有l(wèi)astScrollView、含有superScrollView等等。
而這里會(huì)省略絕大多數(shù)情況下的實(shí)現(xiàn)代碼。
- (void)adjustFrame {UIWindow*keyWindow = [selfkeyWindow];UIViewController*rootController = [_textFieldView topMostController];CGRecttextFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];CGRectrootViewRect = [[rootController view] frame];CGSizekbSize = _kbSize;? ? kbSize.height += keyboardDistanceFromTextField;CGFloattopLayoutGuide =CGRectGetHeight(statusBarFrame);CGFloatmove = MIN(CGRectGetMinY(textFieldViewRect)-(topLayoutGuide+5),CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-kbSize.height));if(move >=0) {? ? ? ? rootViewRect.origin.y -= move;? ? ? ? [selfsetRootViewFrame:rootViewRect];? ? }else{CGFloatdisturbDistance =CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect);if(disturbDistance <0) {? ? ? ? ? ? rootViewRect.origin.y -= MAX(move, disturbDistance);? ? ? ? ? ? [selfsetRootViewFrame:rootViewRect];? ? ? ? }? ? }}
方法- adjustFrame的工作分為兩部分:
計(jì)算move的距離
調(diào)用- setRootViewFrame:方法設(shè)置rootView的大小
- (void)setRootViewFrame:(CGRect)frame {UIViewController*controller = [_textFieldView topMostController];? ? ? ? frame.size = controller.view.frame.size;? ? [UIViewanimateWithDuration:_animationDuration delay:0options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{? ? ? ? [controller.view setFrame:frame];? ? } completion:NULL]; }
不過,在- textFieldViewDidBeginEditing:的調(diào)用棧中,并沒有執(zhí)行- setRootViewFrame:來更新視圖的大小,因?yàn)辄c(diǎn)擊最上面的UITextField時(shí),不需要移動(dòng)視圖就能保證鍵盤不會(huì)遮擋UITextField。
keyboardWillShow:
上面的代碼都是在鍵盤出現(xiàn)之前執(zhí)行的,而這里的- keyboardWillShow:方法的目的是為了保證鍵盤出現(xiàn)之后,依然沒有阻擋UITextField。
因?yàn)槊恳粋€(gè)UITextField對(duì)應(yīng)的鍵盤大小可能不同,所以,這里通過檢測鍵盤大小是否改變,來決定是否調(diào)用- adjustFrame方法更新視圖的大小。
- (void)keyboardWillShow:(NSNotification*)aNotification {? ? _kbShowNotification = aNotification;? ? _animationCurve = [[aNotification userInfo][UIKeyboardAnimationCurveUserInfoKey] integerValue];? ? _animationCurve = _animationCurve<<16;CGFloatduration = [[aNotification userInfo][UIKeyboardAnimationDurationUserInfoKey] floatValue];if(duration !=0.0)? ? _animationDuration = duration;CGSizeoldKBSize = _kbSize;CGRectkbFrame = [[aNotification userInfo][UIKeyboardFrameEndUserInfoKey]CGRectValue];CGRectscreenSize = [[UIScreenmainScreen] bounds];CGRectintersectRect =CGRectIntersection(kbFrame, screenSize);if(CGRectIsNull(intersectRect)) {? ? ? ? _kbSize =CGSizeMake(screenSize.size.width,0);? ? }else{? ? ? ? _kbSize = intersectRect.size;? ? }if(!CGSizeEqualToSize(_kbSize, oldKBSize)) {? ? ? ? [selfadjustFrame];? ? }}
在- adjustFrame方法調(diào)用之前,執(zhí)行了很多代碼都是用來保存一些關(guān)鍵信息的,比如通知對(duì)象、動(dòng)畫曲線、動(dòng)畫時(shí)間。
最關(guān)鍵的是更新鍵盤的大小,然后比較鍵盤的大小CGSizeEqualToSize(_kbSize, oldKBSize)來判斷是否執(zhí)行- adjustFrame方法。
因?yàn)? adjustFrame方法的結(jié)果是依賴于鍵盤大小的,所以這里對(duì)- adjustFrame是有意義并且必要的。
鍵盤的隱藏
通過點(diǎn)擊IQToolBar上面的 done 按鈕,鍵盤就會(huì)隱藏:

IQKeyboardManager-hide-keyboard
鍵盤隱藏的過程中會(huì)依次調(diào)用下面的三個(gè)方法:
- keyboardWillHide:
- textFieldViewDidEndEditing:
- keyboardDidHide:
IQKeyboardManager: ****** keyboardWillHide: started ******IQKeyboardManager: Restoring frame to : {{0,0}, {320,568}}IQKeyboardManager: ****** keyboardWillHide: ended ******IQKeyboardManager: ****** textFieldViewDidEndEditing: started ******IQKeyboardManager: ****** textFieldViewDidEndEditing: ended ******IQKeyboardManager: ****** keyboardDidHide: started ******IQKeyboardManager: ****** keyboardDidHide: ended ******
鍵盤在收起時(shí),需要將視圖恢復(fù)至原來的位置,而這也就是- keyboardWillHide:方法要完成的事情:
[strongSelf.rootViewController.view setFrame:strongSelf.topViewBeginRect]
并不會(huì)給出該方法的全部代碼,只會(huì)給出關(guān)鍵代碼梳理它的工作流程。
在重新設(shè)置視圖的大小以及位置之后,會(huì)對(duì)之前保存的屬性進(jìn)行清理:
_lastScrollView =nil;_kbSize =CGSizeZero;_startingContentInsets =UIEdgeInsetsZero;_startingScrollIndicatorInsets =UIEdgeInsetsZero;_startingContentOffset =CGPointZero;
而之后調(diào)用的兩個(gè)方法- textFieldViewDidEndEditing:以及- keyboardDidHide:也只做了很多簡單的清理工作,包括添加到window上的手勢,并重置保存的UITextField和視圖的大小。
- (void)textFieldViewDidEndEditing:(NSNotification*)notification{? ? [_textFieldView.window removeGestureRecognizer:_tapGesture];? ? _textFieldView =nil;}- (void)keyboardDidHide:(NSNotification*)aNotification {? ? _topViewBeginRect =CGRectZero;}
UITextField 和 UITextView 通知機(jī)制
因?yàn)榭蚣艿墓δ苁腔谕ㄖ獙?shí)現(xiàn)的,所以通知的時(shí)序至關(guān)重要,在IQKeyboardManagerConstants.h文件中詳細(xì)地描述了在編輯UITextField的過程中,通知觸發(fā)的先后順序。

notification-IQKeyboardManage
上圖準(zhǔn)確說明了通知發(fā)出的時(shí)機(jī),透明度為 50% 的部分表示該框架沒有監(jiān)聽這個(gè)通知。
而UITextView的通知機(jī)制與UITextField略有不同:

UITextView-Notification-IQKeyboardManage
當(dāng) Begin Editing 這個(gè)事件發(fā)生時(shí),UITextView的通知機(jī)制會(huì)先發(fā)出UIKeyboardWillShowNotification通知,而UITextField會(huì)先發(fā)出UITextFieldTextDidBeginEditingNotification通知。
而這兩個(gè)通知的方法都調(diào)用了- adjustFrame方法來更新視圖的大小,最開始我并不清楚到底是為什么?直到我給作者發(fā)了一封郵件,作者告訴我這么做的原因:
Good questions draveness. I'm very happy to answer your questions.
There is a file in library IQKeyboardManagerConstants.h. You can find
iOS Notification mechanism structure.
You'll find that for UITextField, textField notification gets fire first and then UIKeyboard notification fires.
For UITextView, UIKeyboard notification gets fire first and then UITextView notification get's fire.
So that's why I have to call adjustFrame at both places to fulfill
both situations. But now I think I should add some validation and make
sure to call it once to improve performance.
Let me know if you have some more questions, I would love to answer them. Thanks again to remind me about this issue.
在不同方法中調(diào)用通知的原因是,UITextView 和 UITextField 通知機(jī)制的不同,不過作者可能會(huì)在未來的版本中修復(fù)這一問題,來獲得性能上的提升。
小結(jié)
IQKeyboardManager使用通知機(jī)制來解決鍵盤遮擋輸入框的問題,因?yàn)槭褂昧朔诸惒⑶以贗QKeyboardManager的+ load方法中激活了框架的使用,所以達(dá)到了零行代碼解決這一問題的效果。
雖然IQKeyboardManager很好地解決了這一問題、為我們帶來了良好的體驗(yàn)。不過,由于其涉及 UI 層級(jí);并且需要考慮非常多的邊界以及特殊條件,框架的代碼不是很容易閱讀,但是這不妨礙IQKeyboardManager成為非常優(yōu)秀的開源項(xiàng)目。
關(guān)注倉庫,及時(shí)獲得更新:iOS-Source-Code-Analyze
Follow:Draveness · Github