IQKeyboardManager 是一个优秀的零行代码解决键盘遮挡的第三方库。在没有看过源码的时候是我认为的最有魔力的第三方库。现在我们就要揭开它的面纱。
使用方式
IQTextView
IQTextView
是一个提供了 placeholder 的 UITextView
。可以设置它文字和颜色:
1 |
|
IQKeyboardReturnKeyHandler
这个文件可以帮助我们将键盘上的 return 键变为 next 键,点击进入下一个输入框。当到最后一个输入框的时候,变为 Done,点击收起键盘。
1 |
|
使用非常简单。只要传入当前 textfield 所在的控制器即可。可以通过设置 IQKeyboardReturnHandler
的 delegate 设置所有 textfield 的 delegate。也可以自己设置每个 textfield 的 delegate。
IQKeyboardManager
在某个页面禁用 IQKeyboardManager
1 | - (void)viewWillAppear:(BOOL)animated{ |
除了上面的直接禁用和启用,IQKeyboardManager 也可以设置在禁用的时候在部分 ViewController 上启用,或者在启动的时候在部分 ViewController 上禁用:
1 | // 在整体禁用的时候可以启动 IQKeyboardManager 的类 |
点击空白处可以隐藏键盘
1 | [IQKeyboardManager sharedManager].shouldResignOnTouchOutside = YES; |
隐藏键盘上的 toolbar
1 | [IQKeyboardManager sharedManager].enableAutoToolbar = NO; |
除了这种一刀切的隐藏或者显示 toolbar 之外,IQKeyboardManager 还提供了两个数组属性,用于标识特例 ViewController:
1 | @property(nonatomic, strong, nonnull, readwrite) NSMutableSet<Class> *disabledToolbarClasses; |
这两个属性分别可以设置在 enableAutoToobar
为 YES 的时候,不显示 toobar 的 ViewController;enableAutoToobar
为 NO 的时候,显示 toobar 的 ViewController。
disableToolbarClasses
默认为:
1 | [UIAlertController, _UIAlertControllerTextFieldViewController] |
源码解析
IQTextView
IQTextView
主要就是在 UITextView
的基础上添加了一个 UILabel
。实现起来也非常简单。
placeholder 主要关注两件事,一是 placeholder 的位置,二是 placeholder 何时隐藏。
首先看 placeholder 的位置,它通过 sizeThatFits
方法获取到占位符的大小:
1 | // 布局方法 |
再看 placeholder 何时隐藏。何时隐藏?只要当前 textView 的 text 不为空就要隐藏。那么怎么知道不为空呢?注册监听 UITextViewTextDidChangeNotification
的通知
1 | -(void)initialize |
分类文件
数组分类
数组分类中包含该两个方法,通过 tag
大小或者通过位置对 UIView 排序:
1 | - (NSArray<UIView*>*)sortedArrayByTag |
这个方法中我们可以学习一个排序的方法:
1 | - (NSArray<UIView*>*)sortedArrayByPosition |
通过 sortedArrayUsingComparator
传入一个比较大小的 block。先根据 y 轴比较大小,y 轴相等的时候再比较 x 轴大小。这样得到的就是一个根据该方法排序的数组。
UIScrollView 分类
UIScrollView 分类中添加了两个分类属性:
1 | // 标识该 UIScrollView 是否可以调整 contentOffset 来达到调整 textfield 位置的目的。 |
UITextField 分类
其实是添加在 UIView 上的几个属性:
1 | // 设置键盘弹出后,textfield 到键盘的距离 |
UIView 分类
UIView 的分类下的方法分为两类,一类是获取 UIView 所在的 UIViewController,一类是获取当前视图下的 UITextField:
1 | // 获得 UIView 的控制器,通过响应链获取 |
IQKeyboardReturnKeyHandler
IQKeyboardReturnKeyHandler 主要用来解决多个 textfield 的时候点击 return 跳到下一个 textfield 的问题。没有看过源码的时候觉得是一个非常神奇的功能。其实实现方式简单点说就是获取当前 UIViewController 内的所有 textfield,然后对他们按照位置排序。点击 return 就让下一个 textfield 获取焦点。
初步处理
初步处理的过程中,会把 UIViewController 中的所有 textfield 全都拿出来,保存基本信息。目的是当 IQKeyboardReturnKeyHandler
实例销毁的时候,将所有 textfield 恢复如初。
首先看入口方法:
1 | // 把 controller 中的所有 textfield 添加到这个类中,所有代理由这个类托管。 |
可以看到一个熟悉的方法 deepResponderViews
,也就是说,初始化的时候,从 UIViewController 中找到了所有的 textfield,并且通过 addTextFieldView
方法把他们保存起来。addTextFieldView
方法把 textfield 的 originalReturnKeyType
delegate
保存了起来,转为了一个 modal,并把它们的 delegate 设置为了自己。这样 textfield 的所有事件都会由 IQKeyboardReturnKeyHandler
实例接管。
1 | // 把 TextField 转为 Modal 添加到 cache 中 |
接管 textfield 的 textfieldDidBeginEditing 方法
IQKeyboardReturnKeyHandler 实例接管了 textfield 的相关方法。在 textfieldDidBeginEditing
中。它会将除最后一个 textfield 之外的所有 textfield 的 returnkeytype 设置为 next,最后一个设置为 return。
具体看代码:
1 | - (void)textFieldDidBeginEditing:(UITextField *)textField |
1 | -(void)updateReturnKeyTypeOnTextField:(UIView*)textField { |
updateReturnKeyTypeOnTextField
方法中将被编辑的 textfield 的兄弟 textfield 全都拿到,然后按照所处位置排序,得到一个排序好的 textFields
兄弟数组,把他们最后一个设置为 Return 类型,其他的都设置为 Next 类型。
其中省略了一部分关于 UITableView 的处理逻辑。设想一下,如果一个 textfield 处于 UITableView 的一个 cell 中,那么应该这个 UITableView 的其他 cell 也会有 textfield。这些 textfield 虽然不是在一个视图中,但应该也是同级的兄弟 textfield。因此,IQKeyboardManager 针对这种情况会对 UITableView 进行深搜,拿到所有的 textfield。
这里省略 UITableView 相关逻辑是因为我们只需要知道设置 textfield 的 returnkeytype 的关键点在于在开始编辑的时候找到所有 textfield 的兄弟 textfield 即可。UITableView 相关逻辑只是对这个目的的补充。
接管 textfield 的 textFieldShouldReturn 方法
和 textfieldDidBeginEditing
相对应的,当点击 next 键的时候,会将焦点置于下一个 textfield:
1 | -(BOOL)textFieldShouldReturn:(UITextField *)textField |
1 | -(BOOL)goToNextResponderOrResign:(UIView*)textField { |
其实理解了设置 returnkeytype 的逻辑,这里设置下一个响应者的逻辑也就明了了。还是获得排序后的 textfield 数组,只要把下一个 textfield 设置为第一响应者就可以了。
IQKeyboardManager
注册
IQKeyboardManager 通过 +(void)load
方法自动创建自身:
1 | +(void)load |
我们常说不要在 load 方法中做太多耗时操作,会影响应用的启动速度。所以,我们可以把要做的初始化操作异步去执行。下面来看初始化方法
注册通知
注册的通知主要包含两部分。一部分是键盘的弹出与隐藏,另一部分是 UITextField
和 UITextView
的编辑的回调。具体通知的处理方法后文解析。
1 | -(void)registerAllNotifications |
注册之外的所有逻辑都在这些通知方法内
创建一个 UITapGestureRecognizer
这个手势用来在点击 UITextField
以外的区域的时候收起键盘
1 | // 为点击屏幕取消第一响应者这个功能创建一个手势 |
配置键盘遮挡相关的一些类
下面的这些配置用于设置 Textfield 在哪些类中 IQKeyboardManager 能够启用,或者关闭。一般使用场景不多。见名思意。
1 | strongSelf.disabledDistanceHandlingClasses = [[NSMutableSet alloc] initWithObjects:[UITableViewController class],[UIAlertController class], nil]; |
textFieldViewDidBeginEditing
在这个开始编辑的方法中,主要就是根据需要添加或移除 toolbar:
判断是否允许添加
首先会调用 privateIsEnableAutoToobar
方法,判断是否需要显示 toobar。这个判断逻辑就是根据外部设置的 enable
变量的值,然后还有前面提到的当前 textfield 所在的 ViewController 是否是 enable
的特例。伪代码如下:
1 | - (Bool) privateIsEnableAutoToobar { |
在 IQToolbar 上添加按钮
如果允许添加。那么就会调用 addToolbarIfRequired
方法。主要是在 toolbar 中添加各种 button。伪代码如下:
1 | - (void)addToolbarIfRequired { |
代码很简单,就是很繁琐,伪代码都写了这么多。可以看到,IQKeyboardManager 很人性化的为 toobar 上左右的按钮都设置了自定义的图片和文字(虽然一般不会有人去改)
按钮点击事件
完成按钮的点击事件可以理解为就是取消 textfield 的第一响应者。prev 按钮和 next 按钮实现上稍微复杂一点,但是思想上是非常简单的。以 prev 按钮为例:
1 | -(void)previousAction:(IQBarButtonItem*)barButton { |
逻辑就是能跳到前面一个就跳到前面一个:
1 | -(BOOL)canGoPrevious { |
有没有很熟悉?和处理 return 键的逻辑类似。拿到所有兄弟 textfield,然后判断当前 textfield 是否是第一个。不是的话就让上一个获取焦点。
移除 IQToolbar
说完了添加 IQToolbar,现在再来快速看一下如果 privateIsEnableAutoToolbar
为 NO 情况下移除 toolbar
1 | -(void)removeToolbarIfRequired { |
移除的方法更简单。直接找到所有兄弟 textfield,如果它们的 inputAccessoryView 是 IQToolbar 类型,那么就清空。
keyboardWillShow
键盘弹出的通知中,就要调整视图偏移了。这个方法中保证了键盘弹出后,我们的键盘不会阻挡 UITextField
拿到键盘弹出的各种参数
主要拿到键盘弹出的动画类型,动画时间以及键盘的大小
1 | -(void)keyboardWillShow:(NSNotification*)aNotification |
这里我稍微修了下代码中的逻辑。因为原来的逻辑中针对多种情况以及 bug 增加了很多对我们了解主流程不必要的代码。
保存 frame 的位置
在键盘弹出调整位置前,把 frame 的原始位置保存起来,这样就可以在键盘收起后,将 frame 移回原来的位置:
1 | _topViewBeginOrigin = rootController.view.frame.origin; |
调整视图偏移
随后来到调整偏移的方法 optimizedAdjustPosition
中,它在主线程中调用。因为届时将会对 UI 进行调整:
1 | -(void)optimizedAdjustPosition{ |
整个 adjustPosition
的方法非常长,还是以伪代码的方式了解一下过程:
1 | - (void)adjustPosition { |
move 的计算有讲究,移动的范围要既不能被 keyboard 挡住,又不能被顶出屏幕显示的范围。所以要比较底下需要往上顶的高度,以及最高能往上多少,取其中较小的。
关于设置 textfield 所在的 scrollView 的 contentOffset。如果你写过类似微信的聊天界面可能有出现过一个问题就是当输入弹出键盘的时候,navigationBar 也移动了上去。这是因为你要输入的瞬间,底部的输入框被 IQKeyboardManager 认为要向上移,但是底部的输入框没有可以滚动的 scrollView,因此就把 rootViewController 的 y 向上移动了,在我们看来就是整个界面都顶了上去。
keyboardWillHide
在 keyboardWillHide 方法中进行扫尾工作,包括将 scrollView 滚回到原来的位置。
1 | - (void)keyboardWillHide:(NSNotification*)aNotification { |
实际上在结束输入的时候,还有其他的事件通知触发,同样也是进行一些扫尾工作,就不做更多的介绍了。
小技巧
到最后了总结一下看 IQKeyboardManager 源码学到的一些技巧。
对一个数组排序:
1 | [SomeViewArray sortedArrayUsingComparator:^NSComparisonResult(UIView *view1, UIView *view2) { |
通过响应链获得当前视图的 ViewController
1 | -(UIViewController*)viewContainingController |
在 load 方法中异步执行初始化操作
1 | +(void)load |
计算方法的执行时间
方法的执行时间可以通过分别获取方法开始执行和执行完毕的时间,然后相减:
1 | CFTimeInterval startTime = CACurrentMediaTime(); |
iOS 发出输入键盘的声音
iOS 提供了一个方法提供播放键盘声音:
1 | [[UIDevice currentDevice] playInputClick] |
比如通讯录选择了首字母可以使用这个方法播放声音。
设置随键盘弹出的视图
在没看代码前,你可能会认为 IQKeyboardManager 是通过动画的方式将 IQToolbar 展示在 keyboard 上的。其实 iOS 提供了相关的属性。可以直接将自定义视图设置为 textfield 的 inputAccessoryView 就可以实现效果:
1 | [textfield setInputAccessView: yourView]; |
总结
看完了 IQKeyboardManager 源码,解决了我一直以来的疑惑。另外,IQKeyboardManager 不愧为一个经久不衰的第三方库。其中为了解决特定情况下的 bug,增加了很多解决 bug 的逻辑和变量,为阅读源码增加了许多难度。不过,这些针对 bug 的逻辑其实不是探寻原理的必要之路,没有必要把一整个工程的代码都理解透彻,跳过它们,可以更快速的定位到库的核心。