iOS源码阅读之IQKeyboardManager
IQKeyboardManager是iOS开发中常用的一个用来处理键盘与输入框间距的优秀第三方库,其使用方法也很简单,只需要导入头文件,无需添加一句代码,就可以使用它来处理键盘输入了.至于深入的使用方法在这里不做赘述,我们今天主要来分析其实现思路
很多人在使用IQKeyboardManager都有可能会被下面这几个问题所困惑
1.我明明一句代码都没有写,IQKeyboardManager是怎样帮我处理键盘输入的呢?
使用+(void)load; 方法
在IQKeyboardManager.m文件中有如下代码
/** Override +load method to enable KeyboardManager when class loader load IQKeyboardManager. Enabling when app starts (No need to write any code) */
+(void)load
{
//Enabling IQKeyboardManager. Loading asynchronous on main thread
[self performSelectorOnMainThread:@selector(sharedManager) withObject:nil waitUntilDone:NO];
}
关于+(void)load; 官方文档给出的解释如下
Declaration
class func load()
Discussion
The load() message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.
The order of initialization is as follows:
All initializers in any framework you link to.
All +load methods in your image.
All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
All initializers in frameworks that link to you.
In addition:
A class’s +load method is called after all of its superclasses’ +load methods.
A category +load method is called after the class’s own +load method.
In a custom implementation of load() you can therefore safely message other unrelated classes from the same image, but any load() methods implemented by those classes may not have run yet.
Important
Custom implementations of the load method for Swift classes bridged to Objective-C are not called automatically.
O__O "…好吧,并看不太懂,百度吧....
load函数是只要你动态加载或者静态引用了这个类,那么load就会被执行,它并不需要你显示的去创建一个类后才会执行,同时只执行一次。
另外就是关于load的执行顺序问题,所有的superclass的load执行完以后才会执行该类的load,以及class中的load方法是先于category中的load执行的。
节选自(https://www.jianshu.com/p/394a072d69e4)
不要打我,嘤嘤嘤~~~(>_<)~~~
2.IQKeyboardManager是如何确保输入框不会被键盘所遮挡的?
IQKeyboardManager在+(void)load;方法中对单例对象进行了初始化,并注册了一系列通知如下
-(void)registerAllNotifications
{
// Registering for keyboard notification.
// 键盘即将出现
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
// 键盘已经出现
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
// 键盘即将隐藏
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
// 键盘已经隐藏
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
// Registering for UITextField notification.
// 监听UITextField UITextFieldTextDidBeginEditingNotification 和 UITextFieldTextDidEndEditingNotification通知
[self registerTextFieldViewClass:[UITextField class]
didBeginEditingNotificationName:UITextFieldTextDidBeginEditingNotification
didEndEditingNotificationName:UITextFieldTextDidEndEditingNotification];
// Registering for UITextView notification.
// 监听UITextView UITextViewTextDidBeginEditingNotification 和 UITextViewTextDidEndEditingNotification 通知
[self registerTextFieldViewClass:[UITextView class]
didBeginEditingNotificationName:UITextViewTextDidBeginEditingNotification
didEndEditingNotificationName:UITextViewTextDidEndEditingNotification];
// Registering for orientation changes notification
// 监听横竖屏切换 并做对应处理
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willChangeStatusBarOrientation:) name:UIApplicationWillChangeStatusBarOrientationNotification object:[UIApplication sharedApplication]];
}
我们可以大致看下keyboardWillShow:方法中大致做了哪些事情
/* UIKeyboardWillShowNotification. */
-(void)keyboardWillShow:(NSNotification*)aNotification
{
_kbShowNotification = aNotification;
// Boolean to know keyboard is showing/hiding
// 一个Bool值判断键盘是否是显示状态
_keyboardShowing = YES;
// Getting keyboard animation.
// 获取键盘弹出的动画
NSInteger curve = [[aNotification userInfo][UIKeyboardAnimationCurveUserInfoKey] integerValue];
_animationCurve = curve<<16;
// Getting keyboard animation duration
// 键盘弹出的动画时间
CGFloat duration = [[aNotification userInfo][UIKeyboardAnimationDurationUserInfoKey] floatValue];
//Saving animation duration
// 存储动画时间
if (duration != 0.0) _animationDuration = duration;
CGRect oldKBFrame = _kbFrame;
// Getting UIKeyboardSize.
_kbFrame = [[aNotification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue];
if ([self privateIsEnabled] == NO) return;
CFTimeInterval startTime = CACurrentMediaTime();
[self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)] indentation:1];
UIView *textFieldView = _textFieldView;
if (textFieldView && CGPointEqualToPoint(_topViewBeginOrigin, kIQCGPointInvalid)) // (Bug ID: #5)
{
// keyboard is not showing(At the beginning only). We should save rootViewRect.
UIViewController *rootController = [textFieldView parentContainerViewController];
_rootViewController = rootController;
if (_rootViewControllerWhilePopGestureRecognizerActive == rootController)
{
_topViewBeginOrigin = _topViewBeginOriginWhilePopGestureRecognizerActive;
}
else
{
_topViewBeginOrigin = rootController.view.frame.origin;
}
_rootViewControllerWhilePopGestureRecognizerActive = nil;
_topViewBeginOriginWhilePopGestureRecognizerActive = kIQCGPointInvalid;
[self showLog:[NSString stringWithFormat:@"Saving %@ beginning origin: %@",rootController,NSStringFromCGPoint(_topViewBeginOrigin)]];
}
//If last restored keyboard size is different(any orientation accure), then refresh. otherwise not.
if (!CGRectEqualToRect(_kbFrame, oldKBFrame))
{
//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 &&
[textFieldView isAlertViewTextField] == NO)
{
// 在这里调整位置
[self optimizedAdjustPosition];
}
}
CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime;
[self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime] indentation:-1];
}
-(void)optimizedAdjustPosition
{
if (_hasPendingAdjustRequest == NO)
{
_hasPendingAdjustRequest = YES;
__weak typeof(self) weakSelf = self;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf adjustPosition];
strongSelf.hasPendingAdjustRequest = NO;
}];
}
}
通过调整scrollview的contentInset 和 contentOffset属性来保证输入框不被屏幕遮挡,该部分的具体实现在-(void)adjustPosition;中,由于这段代码,比较长,而且不太容易理解,就不在这里贴了,有兴趣的同学可以自己去研究下
同时在监听到输入框开始输入之后,如果需要的话,为键盘添加Toolbar
-(void)textFieldViewDidBeginEditing:(NSNotification*)notification
{
CFTimeInterval startTime = CACurrentMediaTime();
[self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)] indentation:1];
// Getting object
// 获取当前输入的UITextField或者UITextView
_textFieldView = notification.object;
UIView *textFieldView = _textFieldView;
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.
// 如果需要添加Toolbar ,则添加
if ([self privateIsEnableAutoToolbar])
{
//UITextView special case. Keyboard Notification is firing before textView notification so we need to reload it's inputViews.
// UITextView是一个特例, 键盘通知将会在textView通知之前,所以需要重载它的inputViews
if ([textFieldView isKindOfClass:[UITextView class]] &&
textFieldView.inputAccessoryView == nil)
{
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:0.00001 delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
__strong typeof(self) strongSelf = weakSelf;
// 添加Toolbar
[strongSelf 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.
// 在UITextView上, 第一次ToolBar不会出现,所以需要这一步
[strongSelf.textFieldView reloadInputViews];
}];
}
//Else adding toolbar
else
{
[self addToolbarIfRequired];
}
}
else
{
[self removeToolbarIfRequired];
}
//Adding Geture recognizer to window (Enhancement ID: #14)
[_resignFirstResponderGesture setEnabled:[self privateShouldResignOnTouchOutside]];
[textFieldView.window addGestureRecognizer:_resignFirstResponderGesture];
if ([self privateIsEnabled] == YES)
{
if (CGPointEqualToPoint(_topViewBeginOrigin, kIQCGPointInvalid)) // (Bug ID: #5)
{
// keyboard is not showing(At the beginning only).
UIViewController *rootController = [textFieldView parentContainerViewController];
_rootViewController = rootController;
if (_rootViewControllerWhilePopGestureRecognizerActive == rootController)
{
_topViewBeginOrigin = _topViewBeginOriginWhilePopGestureRecognizerActive;
}
else
{
_topViewBeginOrigin = rootController.view.frame.origin;
}
_rootViewControllerWhilePopGestureRecognizerActive = nil;
_topViewBeginOriginWhilePopGestureRecognizerActive = kIQCGPointInvalid;
[self showLog:[NSString stringWithFormat:@"Saving %@ beginning origin: %@",rootController, NSStringFromCGPoint(_topViewBeginOrigin)]];
}
//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 &&
[textFieldView isAlertViewTextField] == NO)
{
// keyboard is already showing. adjust frame.
// 调整位置
[self optimizedAdjustPosition];
}
}
// 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] indentation:-1];
}
3.点击Toolbar上的上一个/下一个按钮,IQKeyboardManager如何选定上一个/下一个输入框的?
在IQKeyboardManager中定义了四个方法分别是
/**
Returns YES if can navigate to previous responder textField/textView, otherwise NO.
*/
@property (nonatomic, readonly) BOOL canGoPrevious;
/**
Returns YES if can navigate to next responder textField/textView, otherwise NO.
*/
@property (nonatomic, readonly) BOOL canGoNext;
/**
Navigate to previous responder textField/textView.
*/
- (BOOL)goPrevious;
/**
Navigate to next responder textField/textView.
*/
- (BOOL)goNext;
我们以goNext为例,分析其实现思路
-(BOOL)goNext
{
//Getting all responder view's.
NSArray<__kindof UIView*> *textFields = [self responderViews];
//Getting index of current textField.
NSUInteger index = [textFields indexOfObject:_textFieldView];
//If it is not last textField. then it's next object becomeFirstResponder.
if (index != NSNotFound &&
index < textFields.count-1)
{
UITextField *nextTextField = textFields[index+1];
// Retaining textFieldView
UIView *textFieldRetain = _textFieldView;
BOOL isAcceptAsFirstResponder = [nextTextField becomeFirstResponder];
// If it refuses then becoming previous textFieldView as first responder again. (Bug ID: #96)
if (isAcceptAsFirstResponder == NO)
{
//If next field refuses to become first responder then restoring old textField as first responder.
[textFieldRetain becomeFirstResponder];
[self showLog:[NSString stringWithFormat:@"Refuses to become first responder: %@",nextTextField]];
}
return isAcceptAsFirstResponder;
}
else
{
return NO;
}
}
我们把重点放到第一句,这个方法的作用就是获取与当前的输入框处于同一层级的输入框
NSArray<__kindof UIView*> *textFields = [self responderViews];
其实现代码
/** Get all UITextField/UITextView siblings of textFieldView. */
/// 获取textFieldView的所有兄妹关系的UITextField/UITextView
-(NSArray<__kindof UIView*>*)responderViews
{
UIView *superConsideredView;
UIView *textFieldView = _textFieldView;
//If find any consider responderView in it's upper hierarchy then will get deepResponderView.
// 如果层级之上有需要考虑的情况,则调用deepResponderViews 来获取相关联的UITextField或者UITextView
for (Class consideredClass in _toolbarPreviousNextAllowedClasses)
{
superConsideredView = [textFieldView superviewOfClassType:consideredClass];
if (superConsideredView)
break;
}
//If there is a superConsideredView in view's hierarchy, then fetching all it's subview that responds. No sorting for superConsideredView, it's by subView position. (Enhancement ID: #22)
if (superConsideredView)
{
return [superConsideredView deepResponderViews];
}
//Otherwise fetching all the siblings
else
{
NSArray<UIView*> *textFields = [textFieldView responderSiblings];
//Sorting textFields according to behaviour
switch (_toolbarManageBehaviour)
{
//If autoToolbar behaviour is bySubviews, then returning it.
case IQAutoToolbarBySubviews:
return textFields;
break;
//If autoToolbar behaviour is by tag, then sorting it according to tag property.
case IQAutoToolbarByTag:
return [textFields sortedArrayByTag];
break;
//If autoToolbar behaviour is by tag, then sorting it according to tag property.
case IQAutoToolbarByPosition:
return [textFields sortedArrayByPosition];
break;
default:
return nil;
break;
}
}
}
有两个需要我们去注意的方法
获取superview.subviews中所有的UITextField或者UITextView
- (NSArray<UIView*>*)responderSiblings
{
// Getting all siblings
NSArray<UIView*> *siblings = self.superview.subviews;
//Array of (UITextField/UITextView's).
NSMutableArray<UIView*> *tempTextFields = [[NSMutableArray alloc] init];
for (UIView *textField in siblings)
if ((textField == self || textField.ignoreSwitchingByNextPrevious == NO) && [textField _IQcanBecomeFirstResponder])
[tempTextFields addObject:textField];
return tempTextFields;
}
遍历所有的子视图及更深层视图中的UITextField或UITextView
- (NSArray<UIView*>*)deepResponderViews
{
NSMutableArray<UIView*> *textFields = [[NSMutableArray alloc] init];
for (UIView *textField in self.subviews)
{
if ((textField == self || textField.ignoreSwitchingByNextPrevious == NO) && [textField _IQcanBecomeFirstResponder])
{
[textFields addObject:textField];
}
//Sometimes there are hidden or disabled views and textField inside them still recorded, so we added some more validations here (Bug ID: #458)
//Uncommented else (Bug ID: #625)
if (textField.subviews.count && [textField isUserInteractionEnabled] && ![textField isHidden] && [textField alpha]!=0.0)
{
[textFields addObjectsFromArray:[textField deepResponderViews]];
}
}
//subviews are returning in incorrect order. Sorting according the frames 'y'.
return [textFields sortedArrayUsingComparator:^NSComparisonResult(UIView *view1, UIView *view2) {
CGRect frame1 = [view1 convertRect:view1.bounds toView:self];
CGRect frame2 = [view2 convertRect:view2.bounds toView:self];
CGFloat x1 = CGRectGetMinX(frame1);
CGFloat y1 = CGRectGetMinY(frame1);
CGFloat x2 = CGRectGetMinX(frame2);
CGFloat y2 = CGRectGetMinY(frame2);
if (y1 < y2) return NSOrderedAscending;
else if (y1 > y2) return NSOrderedDescending;
//Else both y are same so checking for x positions
else if (x1 < x2) return NSOrderedAscending;
else if (x1 > x2) return NSOrderedDescending;
else return NSOrderedSame;
}];
return textFields;
}
IQKeyboardManager 通过维持一个数组UITextField或者UITextView的数据,通过index来获取对应的UITextField或者UITextView 来使其响应输入状态