這篇文章會(huì)對(duì) IQKeyboardManager 自動(dòng)解決鍵盤遮擋問(wèn)題的方法進(jìn)行分析。
最近在項(xiàng)目中使用了 IQKeyboardManager 來(lái)解決 UITextField 被鍵盤遮擋的問(wèn)題扁凛,這個(gè)框架的使用方法可以說(shuō)精簡(jiǎn)到了極致答毫,只需要將 IQKeyboardManager 加入 Podfile元镀,然后 pod install 就可以了库正。
說(shuō)是架構(gòu)分析雳灵,其實(shí)只是對(duì) IQKeyboardManager 中包含的類以及文件有一個(gè)粗略地了解哗脖,研究一下這個(gè)項(xiàng)目的層級(jí)是什么樣的。
整個(gè)項(xiàng)目中最核心的部分就是 IQKeyboardManager 這個(gè)類擂啥,它負(fù)責(zé)管理鍵盤出現(xiàn)或者隱藏時(shí)視圖移動(dòng)的距離哄陶,是整個(gè)框架中最核心的部分。
在這個(gè)框架中還有一些用于支持 IQKeyboardManager 的分類哺壶,以及顯示在鍵盤上面的 IQToolBar:
使用紅色標(biāo)記的部分就是 IQToolBar奕筐,左側(cè)的按鈕可以在不同的 UITextField 之間切換,中間的文字是 UITextField.placeholderText变骡,右邊的 Done 應(yīng)該就不需要解釋了。
這篇文章會(huì)主要分析 IQKeyboardManager 中解決的問(wèn)題芭逝,會(huì)用小篇幅介紹包含占位符(Placeholder) IQTextView 的實(shí)現(xiàn)塌碌。
IQTextView 的實(shí)現(xiàn)
在具體研究如何解決鍵盤遮擋問(wèn)題之前,我們先分析一下框架中最簡(jiǎn)單的一部分 IQTextView 是如何為 UITextView 添加占位符的旬盯。
@interface IQTextView : UITextView@endIQTextView 繼承自 UITextView台妆,它只是在 UITextView 上添加上了一個(gè) placeHolderLabel翎猛。在初始化時(shí),我們會(huì)為 UITextViewTextDidChangeNotification 注冊(cè)通知:- (void)initialize? {? ? [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshPlaceholder) name:UITextViewTextDidChangeNotification object:self];}在每次 UITextView 中的 text 更改時(shí)接剩,就會(huì)調(diào)用 refreshPlaceholder 方法更新 placeHolderLabel 的 alpha 值來(lái)隱藏或者顯示 label:
-(void)refreshPlaceholder {? ??
if ([[self text] length]) {? ? ??
? [placeHolderLabel setAlpha:0];? ?
?} else {? ? ? ?
?[placeHolderLabel setAlpha:1];? ?
?}? ??
[self setNeedsLayout];? ?
?[self layoutIfNeeded];
}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)鍵盤的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
注冊(cè)與 UITextField 以及 UITextView 有關(guān)的通知
[self registerTextFieldViewClass:[UITextField class] didBeginEditingNotificationName:UITextFieldTextDidBeginEditingNotification? didEndEditingNotificationName:UITextFieldTextDidEndEditingNotification];
[self registerTextFieldViewClass:[UITextView class] didBeginEditingNotificationName:UITextViewTextDidBeginEditingNotification? didEndEditingNotificationName:UITextViewTextDidEndEditingNotification];
調(diào)用的方法將通知綁定到了 textFieldViewDidBeginEditing: 和 textFieldViewDidEndEditing: 方法上
- (void)registerTextFieldViewClass:(nonnull Class)aClass? didBeginEditingNotificationName:(nonnull NSString *)didBeginEditingNotificationName? ? didEndEditingNotificationName:(nonnull NSString *)didEndEditingNotificationName
?{? ?
?[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidBeginEditing:) name:didBeginEditingNotificationName object:nil];? ??
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidEndEditing:) name:didEndEditingNotificationName object:nil];
}
初始化一個(gè) UITapGestureRecognizer遗座,在點(diǎn)擊 UITextField 對(duì)應(yīng)的 UIWindow 的時(shí)候,收起鍵盤strongSelf.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRecognized:)];
- (void)tapRecognized:(UITapGestureRecognizer*)gesture {? ?
?if (gesture.state == UIGestureRecognizerStateEnded)? ? ? ? [self resignFirstResponder];
}
初始化一些默認(rèn)屬性俊扳,例如鍵盤距離途蒋、覆寫鍵盤的樣式等strongSelf.animationDuration = 0.25;?
strongSelf.animationCurve = UIViewAnimationCurveEaseInOut;
[self setKeyboardDistanceFromTextField:10.0];
[self setShouldPlayInputClicks:YES];
[self setShouldResignOnTouchOutside:NO];
[self setOverrideKeyboardAppearance:NO];
[self setKeyboardAppearance:UIKeyboardAppearanceDefault];
[self setEnableAutoToolbar:YES];
[self setPreventShowingBottomBlankSpace:YES];
[self setShouldShowTextFieldPlaceholder:YES];
[self setToolbarManageBehaviour:IQAutoToolbarBySubviews];
[self setLayoutIfNeededOnUpdate:NO];
設(shè)置不需要解決鍵盤遮擋問(wèn)題的類strongSelf.disabledDistanceHandlingClasses = [[NSMutableSet alloc] initWithObjects:[UITableViewController class], nil];strongSelf.enabledDistanceHandlingClasses = [[NSMutableSet alloc] init];strongSelf.disabledToolbarClasses = [[NSMutableSet alloc] init];strongSelf.enabledToolbarClasses = [[NSMutableSet alloc] init];strongSelf.toolbarPreviousNextAllowedClasses = [[NSMutableSet alloc] initWithObjects:[UITableView class],[UICollectionView class],[IQPreviousNextView class], nil];strongSelf.disabledTouchResignedClasses = [[NSMutableSet alloc] init];strongSelf.enabledTouchResignedClasses = [[NSMutableSet alloc] init];整個(gè)初始化方法大約有幾十行的代碼,在這里就不再展示整個(gè)方法的全部代碼了馋记『牌拢基于通知的解決方案在這里,我們以 UITextField 為例梯醒,分析方法的調(diào)用流程宽堆。在初始化方法中,我們注冊(cè)了很多的通知冤馏,包括鍵盤的出現(xiàn)和隱藏日麸,UITextField 開始編輯與結(jié)束編輯。UIKeyboardWillShowNotification? UIKeyboardWillHideNotification? UIKeyboardDidHideNotification? UITextFieldTextDidBeginEditingNotification? UITextFieldTextDidEndEditingNotification? 在這些通知響應(yīng)時(shí)逮光,會(huì)執(zhí)行以下的方法:| Notification | Selector | |:-:|:-:| | 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 模式在閱讀源代碼的過(guò)程中嗡综,我發(fā)現(xiàn) IQKeyboardManager 提供了 enableDebugging 這一屬性,可以通過(guò)開啟它杜漠,來(lái)追蹤方法的調(diào)用极景,我們可以在 Demo 加入下面這行代碼:[IQKeyboardManager sharedManager].enableDebugging = YES;鍵盤的出現(xiàn)然后運(yùn)行工程,在 Demo 中點(diǎn)擊一個(gè) UITextFieldeasiest-integration-demo上面的操作會(huì)打印出如下所示的 Log:IQKeyboardManager: ****** textFieldViewDidBeginEditing: started ******? IQKeyboardManager: adding UIToolbars if required? IQKeyboardManager: Savingbeginning Frame: {{0, 0}, {320, 568}}? IQKeyboardManager: ****** adjustFrame started ******? IQKeyboardManager: Need to move: -451.00? IQKeyboardManager: ****** adjustFrame ended ******? IQKeyboardManager: ****** textFieldViewDidBeginEditing: ended ******? IQKeyboardManager: ****** keyboardWillShow: started ******? IQKeyboardManager: ****** adjustFrame started ******? IQKeyboardManager: Need to move: -154.00? IQKeyboardManager: ****** adjustFrame ended ******? IQKeyboardManager: ****** keyboardWillShow: ended ******? 我們可以通過(guò)分析 - textFieldViewDidBeginEditing: 以及 - keyboardWillShow: 方法來(lái)了解這個(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 是通過(guò)方法 - addToolbarIfRequired 實(shí)現(xiàn)的,在 - textFieldViewDidBeginEditing: 先通過(guò) - privateIsEnableAutoToolbar 判斷 ToolBar 是否需要添加稍途,再使用相應(yīng)方法 - addToolbarIfRequired 實(shí)現(xiàn)這一目的阁吝。這個(gè)方法會(huì)根據(jù)根視圖上 UITextField 的數(shù)量執(zhí)行對(duì)應(yīng)的代碼,下面為一般情況下執(zhí)行的代碼:- (void)addToolbarIfRequired {? ? NSArray *siblings = [self responderViews];? ? for (UITextField *textField in siblings) {? ? ? ? [textField addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:) shouldShowPlaceholder:_shouldShowTextFieldPlaceholder];? ? ? ? textField.inputAccessoryView.tag = kIQPreviousNextButtonToolbarTag;? ? ? ? IQToolbar *toolbar = (IQToolbar*)[textField inputAccessoryView];? ? ? ? toolbar.tintColor = [UIColor blackColor];? ? ? ? [toolbar setTitle:textField.drawingPlaceholderText];? ? ? ? [textField setEnablePrevious:NO next:YES];? ? }}在鍵盤上的 IQToolBar 一般由三部分組成:切換 UITextField 的箭頭按鈕指示當(dāng)前 UITextField 的 placeholderDone ButtonIQToolBarIte這些 item 都是 IQBarButtonItem 的子類這些 IQBarButtonItem 以及 IQToolBar 都是通過(guò)方法 - 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:UIBarButtonItemStylePlain target:target action:previousAction];? ? IQBarButtonItem *next = [[IQBarButtonItem alloc] initWithImage:imageRightArrow style:UIBarButtonItemStylePlain target:target action:nextAction];? ? IQTitleBarButtonItem *title = [[IQTitleBarButtonItem alloc] initWithTitle:self.shouldHideTitle?nil:titleText];? ? IQBarButtonItem *doneButton =[[IQBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:target action:doneAction];? ? IQToolbar *toolbar = [[IQToolbar alloc] init];? ? toolbar.barStyle = UIBarStyleDefault;? ? toolbar.items = @[prev, next, title, doneButton];? ? toolbar.titleInvocation = self.titleInvocation;? ? [(UITextField*)self setInputAccessoryView:toolbar];}上面是方法簡(jiǎn)化后的實(shí)現(xiàn)代碼械拍,初始化需要的 IQBarButtonItem突勇,然后將這些 IQBarButtonItem 全部加入到 IQToolBar 上,最后設(shè)置 UITextField 的 accessoryView殊者。保存 frame這一步的主要目的是為了在鍵盤隱藏時(shí)恢復(fù)到原來(lái)的狀態(tài)与境,其實(shí)現(xiàn)也非常簡(jiǎn)單:_rootViewController = [_textFieldView topMostController];? _topViewBeginRect = _rootViewController.view.frame;? 獲取 topMostController,在 _topViewBeginRect 中保存 frame猖吴。adjustFrame在上述的任務(wù)都完成之后摔刁,最后就需要調(diào)用 - adjustFrame 方法來(lái)調(diào)整當(dāng)前根試圖控制器的 frame 了:我們只會(huì)研究一般情況下的實(shí)現(xiàn)代碼,因?yàn)檫@個(gè)方法大約有 400 行代碼對(duì)不同情況下的實(shí)現(xiàn)有不同的路徑海蔽,包括有 lastScrollView共屈、含有 superScrollView 等等。而這里會(huì)省略絕大多數(shù)情況下的實(shí)現(xiàn)代碼党窜。- (void)adjustFrame {? ? UIWindow *keyWindow = [self keyWindow];? ? UIViewController *rootController = [_textFieldView topMostController];? ? ? ? CGRect textFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];? ? CGRect rootViewRect = [[rootController view] frame];? ? CGSize kbSize = _kbSize;? ? kbSize.height += keyboardDistanceFromTextField;? ? CGFloat topLayoutGuide = CGRectGetHeight(statusBarFrame);? ? CGFloat move = MIN(CGRectGetMinY(textFieldViewRect)-(topLayoutGuide+5), CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-kbSize.height));? ? if (move >= 0) {? ? ? ? rootViewRect.origin.y -= move;? ? ? ? [self setRootViewFrame:rootViewRect];? ? } else {? ? ? ? CGFloat disturbDistance = CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect);? ? ? ? if (disturbDistance < 0) {? ? ? ? ? ? rootViewRect.origin.y -= MAX(move, disturbDistance);? ? ? ? ? ? [self setRootViewFrame:rootViewRect];? ? ? ? }? ? }}方法 - adjustFrame 的工作分為兩部分:計(jì)算 move 的距離調(diào)用 - setRootViewFrame: 方法設(shè)置 rootView 的大小- (void)setRootViewFrame:(CGRect)frame {? ? UIViewController *controller = [_textFieldView topMostController];? ? ? ? frame.size = controller.view.frame.size;[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{? ? [controller.view setFrame:frame];} completion:NULL];}不過(guò)拗引,在 - textFieldViewDidBeginEditing: 的調(diào)用棧中,并沒有執(zhí)行 - setRootViewFrame: 來(lái)更新視圖的大小幌衣,因?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)的鍵盤大小可能不同楚里,所以断部,這里通過(guò)檢測(cè)鍵盤大小是否改變,來(lái)決定是否調(diào)用 - adjustFrame 方法更新視圖的大小班缎。
- (void)keyboardWillShow:(NSNotification*)aNotification {? ? _kbShowNotification = aNotification;? ? _animationCurve = [[aNotification userInfo][UIKeyboardAnimationCurveUserInfoKey] integerValue];? ? _animationCurve = _animationCurve<<16;? ? CGFloat duration = [[aNotification userInfo][UIKeyboardAnimationDurationUserInfoKey] floatValue];? ? if (duration != 0.0)? ? _animationDuration = duration;? ? CGSize oldKBSize = _kbSize;? ? CGRect kbFrame = [[aNotification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue];? ? CGRect screenSize = [[UIScreen mainScreen] bounds];? ? CGRect intersectRect = CGRectIntersection(kbFrame, screenSize);? ? if (CGRectIsNull(intersectRect)) {? ? ? ? _kbSize = CGSizeMake(screenSize.size.width, 0);? ? } else {? ? ? ? _kbSize = intersectRect.size;? ? }? ? if (!CGSizeEqualToSize(_kbSize, oldKBSize)) {? ? ? ? [self adjustFrame];? ? }}在 - adjustFrame 方法調(diào)用之前蝴光,執(zhí)行了很多代碼都是用來(lái)保存一些關(guān)鍵信息的,比如通知對(duì)象达址、動(dòng)畫曲線蔑祟、動(dòng)畫時(shí)間。最關(guān)鍵的是更新鍵盤的大小沉唠,然后比較鍵盤的大小 CGSizeEqualToSize(_kbSize, oldKBSize) 來(lái)判斷是否執(zhí)行 - adjustFrame 方法做瞪。因?yàn)?- adjustFrame 方法的結(jié)果是依賴于鍵盤大小的,所以這里對(duì) - adjustFrame 是有意義并且必要的。鍵盤的隱藏通過(guò)點(diǎn)擊 IQToolBar 上面的 done 按鈕装蓬,鍵盤就會(huì)隱藏:IQKeyboardManager-hide-keyboard鍵盤隱藏的過(guò)程中會(huì)依次調(diào)用下面的三個(gè)方法:- keyboardWillHide:- textFieldViewDidEndEditing:- keyboardDidHide:IQKeyboardManager: ****** keyboardWillHide: started ******? IQKeyboardManager: Restoringframe to : {{0, 0}, {320, 568}}
IQKeyboardManager: ****** keyboardWillHide: ended ******
IQKeyboardManager: ****** textFieldViewDidEndEditing: started ******
IQKeyboardManager: ****** textFieldViewDidEndEditing: ended ******
IQKeyboardManager: ****** keyboardDidHide: started ******
IQKeyboardManager: ****** keyboardDidHide: ended ******
鍵盤在收起時(shí),需要將視圖恢復(fù)至原來(lái)的位置纱扭,而這也就是 - 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: 也只做了很多簡(jiǎn)單的清理工作暗赶,包括添加到 window 上的手勢(shì),并重置保存的 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 的過(guò)程中因惭,通知觸發(fā)的先后順序岳锁。
notification-IQKeyboardManage
上圖準(zhǔn)確說(shuō)明了通知發(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 方法來(lái)更新視圖的大小乒躺,最開始我并不清楚到底是為什么?直到我給作者發(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ī)制的不同,不過(guò)作者可能會(huì)在未來(lái)的版本中修復(fù)這一問(wèn)題咆繁,來(lái)獲得性能上的提升讳推。
小結(jié)
IQKeyboardManager 使用通知機(jī)制來(lái)解決鍵盤遮擋輸入框的問(wèn)題,因?yàn)槭褂昧朔诸惒⑶以?IQKeyboardManager 的 + load 方法中激活了框架的使用么介,所以達(dá)到了零行代碼解決這一問(wèn)題的效果娜遵。
雖然 IQKeyboardManager 很好地解決了這一問(wèn)題、為我們帶來(lái)了良好的體驗(yàn)壤短。不過(guò)设拟,由于其涉及 UI 層級(jí);并且需要考慮非常多的邊界以及特殊條件久脯,框架的代碼不是很容易閱讀纳胧,但是這不妨礙 IQKeyboardManager 成為非常優(yōu)秀的開源項(xiàng)目。