項(xiàng)目中鍵盤遮擋輸入框的問題我想一直都是讓人頭疼的問題 但是有了強(qiáng)大的IQKeyboardManager 這個(gè)框架 這個(gè)問題就不再是問題了
好了 以下就是本人對這個(gè)框架的一些理解
一 使用方法
這個(gè)框架的使用方法也是極其的簡單
只需要將 IQKeyboardManager 加入 Podfile,然后 pod install 就可以了。
pod 'IQKeyboardManager'
// iOS delegate內(nèi)應(yīng)用入口
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//關(guān)閉設(shè)置為NO, 默認(rèn)值為NO.
[IQKeyboardManager sharedManager ].enable = YES;
}
如果產(chǎn)品需要當(dāng)鍵盤彈起時(shí)盟戏,點(diǎn)擊背景收起鍵盤,也是一行代碼解決馏鹤。
[IQKeyboardManager sharedManager].shouldResignOnTouchOutside = YES;
而當(dāng)產(chǎn)品需要支持內(nèi)聯(lián)編輯(Inline Editing), 這就需要隱藏鍵盤上的工具條(默認(rèn)打開)
[IQKeyboardManager sharedManager].enableAutoToolbar = NO;
如果當(dāng)某一個(gè)輸入框特定不需要鍵盤上的工具條時(shí)搀缠,一行代碼
textField.inputAccessoryView = [[UIView alloc] init];
如果因?yàn)椴恢脑蛐枰谀硞€(gè)頁面禁止自動(dòng)鍵盤處理事件相應(yīng),也很簡單铝宵。
- (void) viewWillAppear: (BOOL)animated {
//打開鍵盤事件相應(yīng)
[IQKeyboardManager sharedManager].enable = YES;
}
- (void) viewWillDisappear: (BOOL)animated {
//關(guān)閉鍵盤事件相應(yīng)
[IQKeyboardManager sharedManager].enable = NO;
}
二 源碼分析
我們先看一下 IQKeyBoardManager中類的層級關(guān)系
整個(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)該就不需要解釋了窘游。
這篇文章會主要分析 IQKeyboardManager 中解決的問題,會用小篇幅介紹包含占位符(Placeholder) IQTextView 的實(shí)現(xiàn)跳纳。
(1). IQTextView的實(shí)現(xiàn)
在具體研究如何解決鍵盤遮擋問題之前忍饰,我們先分析一下框架中最簡單的一部分 IQTextView 是如何為 UITextView 添加占位符的。
@interface IQTextView : UITextView
@end
IQTextView 繼承自 UITextView寺庄,它只是在 UITextView 上添加上了一個(gè) placeHolderLabel艾蓝。
在初始化時(shí),我們會為 UITextViewTextDidChangeNotification 注冊通知:
- (void)initialize {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(refreshPlaceholder) name:UITextViewTextDidChangeNotification object:self];
}
在每次 UITextView 中的 text 更改時(shí)斗塘,就會調(diào)用 refreshPlaceholder 方法更新 placeHolderLabel 的 alpha 值來隱藏或者顯示 label:
-(void)refreshPlaceholder {
if ([[self text] length]) {
[placeHolderLabel setAlpha:0];
} else {
[placeHolderLabel setAlpha:1];
}
[self setNeedsLayout];
[self layoutIfNeeded];
}
IQKeyboardManager
下面就會進(jìn)入這篇文章的正題:IQKeyboardManager赢织。
如果你對 iOS 開發(fā)比較熟悉,可能會發(fā)現(xiàn)每當(dāng)一個(gè)類的名字中包含了 manager馍盟,那么這個(gè)類可能可能遵循單例模式于置,IQKeyboardManager 也不例外
IQKeyboardManager 的初始化
當(dāng) IQKeyboardManager 初始化的時(shí)候,它做了這么幾件事情:
1 監(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];
2 注冊與 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];
}
3 初始化一個(gè) UITapGestureRecognizer贞岭,在點(diǎn)擊 UITextField 對應(yīng)的 UIWindow 的時(shí)候八毯,收起鍵盤
strongSelf.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRecognized:)];
- (void)tapRecognized:(UITapGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateEnded)
[self resignFirstResponder];
}
4 初始化一些默認(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];
5 設(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];
基于通知的解決方案
在這里话速,我們以 UITextField 為例讶踪,分析方法的調(diào)用流程
在初始化方法中,我們注冊了很多的通知泊交,包括鍵盤的出現(xiàn)和隱藏乳讥,UITextField 開始編輯與結(jié)束編輯。
在這些通知響應(yīng)時(shí)廓俭,會執(zhí)行以下的方法
通知對應(yīng)的方法
UIKeyboardWillShowNotification ------> @selector(keyboardWillShow:)
UIKeyboardWillHideNotification ------> @selector(keyboardWillHide:)
UIKeyboardDidHideNotification ------> @selector(keyboardDidHide:)
UITextFieldTextDidBeginEditingNotification ------> @selector(textFieldViewDidBeginEditing:)
UITextFieldTextDidEndEditingNotification ------> @selector(textFieldViewDidEndEditing:)
整個(gè)解決方案其實(shí)都是基于 iOS 中的通知系統(tǒng)的云石;在事件發(fā)生時(shí),調(diào)用對應(yīng)的方法做出響應(yīng)研乒。
開啟 Debug 模式
在閱讀源代碼的過程中留晚,我發(fā)現(xiàn) IQKeyboardManager 提供了 enableDebugging 這一屬性,可以通過開啟它告嘲,來追蹤方法的調(diào)用,我們可以在 Demo 加入下面這行代碼
[IQKeyboardManager sharedManager].enableDebugging = YES;
運(yùn)行demo
點(diǎn)擊圖中的textField 上邊的操作會打印出如下所示的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,它做了三件事情:
1為 UITextField 添加 IQToolBar
2在調(diào)整 frame 前犬庇,保存當(dāng)前 frame僧界,以備之后鍵盤隱藏后的恢復(fù)
3調(diào)用 - adjustFrame 方法,將視圖移動(dòng)到合適的位置
添加 ToolBar
添加 ToolBar 是通過方法 - addToolbarIfRequired
實(shí)現(xiàn)的臭挽,在 - textFieldViewDidBeginEditing:
先通過 - privateIsEnableAutoToolbar
判斷 ToolBar 是否需要添加捂襟,再使用相應(yīng)方法 - addToolbarIfRequired
實(shí)現(xiàn)這一目的。
這個(gè)方法會根據(jù)根視圖上 UITextField 的數(shù)量執(zhí)行對應(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 一般由三部分組成:
1 切換 UITextField 的箭頭按鈕
2 指示當(dāng)前 UITextField 的 placeholder
3 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
了:
我們只會研究一般情況下的實(shí)現(xiàn)代碼,因?yàn)檫@個(gè)方法大約有 400 行代碼對不同情況下的實(shí)現(xiàn)有不同的路徑室囊,包括有 lastScrollView雕崩、含有 superScrollView 等等魁索。
而這里會省略絕大多數(shù)情況下的實(shí)現(xiàn)代碼。
我們只會研究一般情況下的實(shí)現(xiàn)代碼晨逝,因?yàn)檫@個(gè)方法大約有 400 行代碼對不同情況下的實(shí)現(xiàn)有不同的路徑蛾默,包括有 lastScrollView、含有 superScrollView 等等捉貌。
而這里會省略絕大多數(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
的工作分為兩部分
1 計(jì)算 move
的距離
2 調(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)視圖就能保證鍵盤不會遮擋 UITextField醒陆。
keyboardWillShow:
上面的代碼都是在鍵盤出現(xiàn)之前執(zhí)行的瀑构,而這里的 - keyboardWillShow:
方法的目的是為了保證鍵盤出現(xiàn)之后,依然沒有阻擋 UITextField
刨摩。
因?yàn)槊恳粋€(gè) UITextField
對應(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)鍵信息的罢浇,比如通知對象陆赋、動(dòng)畫曲線、動(dòng)畫時(shí)間嚷闭。
最關(guān)鍵的是更新鍵盤的大小攒岛,然后比較鍵盤的大小 CGSizeEqualToSize(_kbSize, oldKBSize)
來判斷是否執(zhí)行 - adjustFrame
方法。
因?yàn)?- adjustFrame 方法的結(jié)果是依賴于鍵盤大小的胞锰,所以這里對 - adjustFrame 是有意義并且必要的灾锯。
鍵盤的隱藏
通過點(diǎn)擊 IQToolBar
上面的done
按鈕,鍵盤就會隱藏
鍵盤隱藏的過程中會依次調(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]
在重新設(shè)置視圖的大小以及位置之后,會對之前保存的屬性進(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;
}
小結(jié)
IQKeyboardManager
使用通知機(jī)制來解決鍵盤遮擋輸入框的問題案怯,因?yàn)槭褂昧朔诸惒⑶以?code>IQKeyboardManager 的 + load 方法中激活了框架的使用君旦,所以達(dá)到了零行代碼解決這一問題的效果。
雖然 IQKeyboardManager
很好地解決了這一問題、為我們帶來了良好的體驗(yàn)金砍。不過局蚀,由于其涉及 UI 層級;并且需要考慮非常多的邊界以及特殊條件恕稠,框架的代碼不是很容易閱讀琅绅,但是這不妨礙IQKeyboardManager
成為非常優(yōu)秀的開源項(xiàng)目
<部分借鑒網(wǎng)絡(luò)>