關(guān)注倉庫莽红,及時(shí)獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github
這篇文章會(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í)是什么樣的花枫。
整個(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
中解決的問題溺蕉,會(huì)用小篇幅介紹包含占位符(Placeholder) IQTextView
的實(shí)現(xiàn)伶丐。
IQTextView 的實(shí)現(xiàn)
在具體研究如何解決鍵盤遮擋問題之前,我們先分析一下框架中最簡單的一部分 IQTextView
是如何為 UITextView
添加占位符的疯特。
@interface IQTextView : UITextView
@end
IQTextView
繼承自 UITextView
哗魂,它只是在 UITextView
上添加上了一個(gè) placeHolderLabel
。
在初始化時(shí)漓雅,我們會(huì)為 UITextViewTextDidChangeNotification
注冊通知:
- (void)initialize {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshPlaceholder) name:UITextViewTextDidChangeNotification object:self];
}
在每次 UITextView 中的 text 更改時(shí)录别,就會(huì)調(diào)用 refreshPlaceholder
方法更新 placeHolderLabel
的 alpha
值來隱藏或者顯示 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];
-
注冊與
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è)置不需要解決鍵盤遮擋問題的類
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)用流程讯私。
在初始化方法中,我們注冊了很多的通知西傀,包括鍵盤的出現(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 模式
在閱讀源代碼的過程中,我發(fā)現(xiàn) IQKeyboardManager
提供了 enableDebugging
這一屬性间雀,可以通過開啟它悔详,來追蹤方法的調(diào)用,我們可以在 Demo 加入下面這行代碼:
[IQKeyboardManager sharedManager].enableDebugging = YES;
鍵盤的出現(xiàn)
然后運(yùn)行工程惹挟,在 Demo 中點(diǎn)擊一個(gè) UITextField
上面的操作會(huì)打印出如下所示的 Log:
IQKeyboardManager: ****** textFieldViewDidBeginEditing: started ******
IQKeyboardManager: adding UIToolbars if required
IQKeyboardManager: Saving <UINavigationController 0x7f905b01b000> beginning 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 ******
我們可以通過分析 - 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 = [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
的 placeholder - Done Button
這些 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: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];
}
上面是方法簡化后的實(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)有不同的路徑榜晦,包括有
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]; }
不過,在
- 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;
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í)行了很多代碼都是用來保存一些關(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ì)隱藏:
鍵盤隱藏的過程中會(huì)依次調(diào)用下面的三個(gè)方法:
- keyboardWillHide:
- textFieldViewDidEndEditing:
- keyboardDidHide:
IQKeyboardManager: ****** keyboardWillHide: started ******
IQKeyboardManager: Restoring <UINavigationController 0x7fbaa4009e00> 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ā)的先后順序涧尿。
上圖準(zhǔn)確說明了通知發(fā)出的時(shí)機(jī),透明度為 50% 的部分表示該框架沒有監(jiān)聽這個(gè)通知檬贰。
而 UITextView
的通知機(jī)制與 UITextField
略有不同:
當(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)槭褂昧朔诸惒⑶以?IQKeyboardManager
的 + load
方法中激活了框架的使用赁严,所以達(dá)到了零行代碼解決這一問題的效果。
雖然 IQKeyboardManager
很好地解決了這一問題粉铐、為我們帶來了良好的體驗(yàn)疼约。不過,由于其涉及 UI 層級(jí)蝙泼;并且需要考慮非常多的邊界以及特殊條件程剥,框架的代碼不是很容易閱讀,但是這不妨礙 IQKeyboardManager
成為非常優(yōu)秀的開源項(xiàng)目。
關(guān)注倉庫织鲸,及時(shí)獲得更新:iOS-Source-Code-Analyze
Follow: Draveness · Github