互联网科技

IQKeyboardManager源码分析

作者:金沙国际官网    发布时间:2020-04-20 07:27     浏览次数 :123

[返回]

我们项目中很经常会碰到输入框键盘的遮挡问题,写多了我们会发现,不同视图和场景下处理逻辑大同小异,都是先注册监听键盘的弹起隐藏事件,然后在弹起隐藏的时候做处理,之后再移除键盘相关监听,因此你会发现,重复性代码挺多;虽然现在已经有了IQKeyboardManager这样优秀的第三方来管理,但有时候又会觉得IQKeyboardManager过于重量级,功能过于庞大,你可能不想引入这么庞大第三方,出于这个原因我自己写了一个键盘管理辅助类,来处理键盘输入框遮挡事件,核心代码一百多行处理只需要一句话就可以解决。

写作原因:看三方库源码总不知道该看什么或者能学到什么,写文章无疑是最好的药;并且我在简书上搜了一下相关文章都不是很全面,所以就寻思着自己写一篇;本人能力有限,需要大家一起思考并完善本超长文

这几天项目的新需求中有个复杂的表单界面,在做的过程中发现要比想象中复杂很多,有好多问题需要处理。有很多东西值得写下来好好梳理下。

  • 使用方法

IQKeyboardManager是一个自动解决键盘遮挡输入源的库,输入源目前只有UITextView和UITextField;解决方法是让界面内容上移到合适位置让输入框在键盘之上

需求分析:

图片 16创建网店1.png

上图便是UI根据需求给的高保真, 我们先根据这张图片来描述一下具体需求,明确一下我们都需要干些什么。

创建网店这个界面是一个复杂的表单,有“网店名称”、“网店主标签”、“网店简介”、“网店地址”、“网店座机”、“email”、“网店LOGO”、“网店封面图”这些项。大部分都是输入框,但也有几项有所不同。“网店地址”项,当被点击后会弹出一个pickView来选择“市&区”;“网店LOGO”和“网店封面图”是一样的,是选取图片的控件,要求既可以通过相册选取图片,也可以现场拍照选择。当被点击后,弹出一个ActionSheet来是以“拍照”或以“相册”来选取图片。当选取成功后拍照的背景图片变为被选取的图片,并在右上角出现一个删除按钮,可以删除还原再次选取。

表单中除了“email”外所有的项目都是必填的,且“网店名称”、“网店主标签”、“网店简介”和“网店座机”分别有30、20、500、15字的长度限制。“email”虽然为选填,但若填写了则会进行邮箱格式校验。对字数长度的限制要在输入过程中进行监听,若输入时超过限制,则输入框出现红色边框并出现提示文字。等最后点击了“提交”按钮后要进行数据校验,所有该填但未填,所有格式不正确的项都会出现红框和提示文字,当所有数据都合法后才可以提交给服务器。

需求大体就是如此。

这个界面我们还是以tableView来实现,由cell视图来表示图中所需填写的项目。那我们得先分析下这个界面需要写哪几种样式的cell

该界面总共有4种样式的cell。4种样式的cell样式也有共同点,每个cell左边部分均为表示该行所要填写的项目名称,右边部分则为填写或者选取的内容值,这些值的显示形式有所不同。 CreateShopTFCellCreateShopTVCell其实非常类似,右边首先是一个灰色的背景视图,只不过在灰色背景之上的前者是textField,而后者是textViewCreateShopPickCell右边则是两个灰色背景视图,点击之后便弹出一个pickView供你选取“市&区”;CreateShopUploadPicCell右边则是一个UIImageView,无图片被选取时默认是一个相机的图片,当被点击后弹出ActionSheet供你选择拍照还是从相册选取照片,选好照片后UIImageView的图片被替换,并在右上角出现红色的删除按钮。

如下图所示:

图片 26创建网店.png

pod集成

只需要在Podfile中写上:

pod 'IQKeyboardManager', '~> 4.0.7'

然后加入工程:

 pod install --verbose --no-repo-update

正确地将视图和数据绑定:

我们假设已经写好了上面4种样式cell的代码,现在我们在控制器里为其填充数据。

我们首先定义一个表示cell数据的CreateShopModel。该model是为了给cell填充数据,可以看到它里面的属性就是cell上对应应该显示的数据项。同时,我们在开头也定义了一个枚举CreateShopCellType来代表4种不同样式的cell,用于在tableView返回cell的代理方法里根据枚举值来返回相应样式的cell

#import <Foundation/Foundation.h>typedef enum : NSUInteger { CreateShopCellType_TF = 0, // textfield CreateShopCellType_TV, // textView CreateShopCellType_PICK, // picker CreateShopCellType_PIC, // upload picture} CreateShopCellType;@interface CreateShopModel : NSObject@property (nonatomic, copy)NSString *title; // 所要填写的项目名称@property (nonatomic, copy)NSString *placeholder;@property (nonatomic, copy)NSString *key; // 表单对应的字段@property (nonatomic, copy)NSString *errText; // 校验出错时的提示信息@property (nonatomic, strong)UIImage *image; // 所选取的图片@property (nonatomic, assign)CreateShopCellType cellType; // cell的类型@property (nonatomic, assign)NSInteger maxInputLength; // 最大输入长度限制@end

我们在将tableView创建并添加在控制器的view上后便可以初始化数据源了。该界面tableView的数据源是_tableViewData数组,数据的每项元素是代表cell显示数据的CreateShopModel类型的model。准确地来说,这些数据是表单未填写之前的死数据,所以需要我们手动地给装入数据源数组中。而在输入框输入或者选取而得的数据则需要我们在输入之后将其捕获存储下来,以等到提交时提交给服务器,这个也有需要注意的坑点,后面再说。

- intDataSource{ _tableViewData = [NSMutableArray array]; CreateShopModel *nameModel = [[CreateShopModel alloc] init]; nameModel.title = @"网店名称"; nameModel.placeholder = @"请输入网店名称"; nameModel.key = @"groupName"; nameModel.cellType = CreateShopCellType_TF; nameModel.maxInputLength = 30; [_tableViewData addObject:nameModel]; CreateShopModel *mainTagModel = [[CreateShopModel alloc] init]; mainTagModel.title = @"网店主标签"; mainTagModel.placeholder = @"请输入网店主标签"; mainTagModel.key = @"tag"; mainTagModel.cellType = CreateShopCellType_TF; mainTagModel.maxInputLength = 20; [_tableViewData addObject:mainTagModel]; CreateShopModel *descModel = [[CreateShopModel alloc] init]; descModel.title = @"网店简介"; descModel.placeholder = @"请输入网店简介"; descModel.key = @"introduction"; descModel.cellType = CreateShopCellType_TV; descModel.maxInputLength = 500; [_tableViewData addObject:descModel]; CreateShopModel *addressModel = [[CreateShopModel alloc] init]; addressModel.title = @"网店地址"; addressModel.placeholder = @""; addressModel.key = @"regionId"; addressModel.cellType = CreateShopCellType_PICK; [_tableViewData addObject:addressModel]; CreateShopModel *doorIDModel = [[CreateShopModel alloc] init]; doorIDModel.title = @""; doorIDModel.placeholder = @"请输入详细门牌号"; doorIDModel.key = @"address"; doorIDModel.cellType = CreateShopCellType_TF; [_tableViewData addObject:doorIDModel]; CreateShopModel *phoneModel = [[CreateShopModel alloc] init]; phoneModel.title = @"网店座机"; phoneModel.placeholder = @"请输入网店座机"; phoneModel.key = @"telephone"; phoneModel.cellType = CreateShopCellType_TF; phoneModel.maxInputLength = 15; [_tableViewData addObject:phoneModel]; CreateShopModel *emailModel = [[CreateShopModel alloc] init]; emailModel.title = @"email"; emailModel.placeholder = @"请输入email"; emailModel.key = @"contactMail"; emailModel.cellType = CreateShopCellType_TF; [_tableViewData addObject:emailModel]; CreateShopModel *logoModel = [[CreateShopModel alloc] init]; logoModel.title = @"网店LOGO"; logoModel.placeholder = @""; logoModel.key = @"logo"; logoModel.urlKey = @"logoUrl"; logoModel.cellType = CreateShopCellType_PIC; [_tableViewData addObject:logoModel]; CreateShopModel *coverPicModel = [[CreateShopModel alloc] init]; coverPicModel.title = @"网店封面图"; coverPicModel.placeholder = @""; coverPicModel.key = @"cover"; coverPicModel.urlKey = @"coverUrl"; coverPicModel.cellType = CreateShopCellType_PIC; [_tableViewData addObject:coverPicModel]; if(_tableView){ [_tableView reloadData]; }}

现在我们的数据源准备好了,但是tableView还没做处理呢,要等tableView也配套完成后再刷新tableView就OK了。我们来看tableView代理方法。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return _tableViewData.count;}- tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ CreateShopModel *createModel = _tableViewData[indexPath.row]; if(createModel.cellType == CreateShopCellType_TF){ return [CreateShopTFCell cellHeight:createModel]; }else if(createModel.cellType == CreateShopCellType_TV){ return [CreateShopTVCell cellHeight:createModel]; }else if(createModel.cellType == CreateShopCellType_PICK){ return [CreateShopPickCell cellHeight:createModel]; }else if(createModel.cellType == CreateShopCellType_PIC){ return [CreateShopUploadPicCell cellHeight: createModel]; } return 50.f;}- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ CreateShopModel *createModel = _tableViewData[indexPath.row]; if(createModel.cellType == CreateShopCellType_TF) { static NSString *tfCellId = @"tfCellId"; CreateShopTFCell *cell = [tableView dequeueReusableCellWithIdentifier:tfCellId]; if(cell==nil) { cell = [[CreateShopTFCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tfCellId]; cell.cellDelegate = self; } [cell refreshContent:createModel formModel:_shopFormModel]; return cell; } else if(createModel.cellType == CreateShopCellType_TV) { static NSString *tvCellId = @"tvCellId"; CreateShopTVCell *cell = [tableView dequeueReusableCellWithIdentifier:tvCellId]; if(cell==nil) { cell = [[CreateShopTVCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:tvCellId]; } [cell refreshContent:createModel formModel:_shopFormModel]; return cell; } else if(createModel.cellType == CreateShopCellType_PICK) { static NSString *pickCellId = @"pickCellId"; CreateShopPickCell *cell = [tableView dequeueReusableCellWithIdentifier:pickCellId]; if(cell==nil) { cell = [[CreateShopPickCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:pickCellId]; } NSString *valueStr = [_shopFormModel valueForKey:createModel.key]; if(valueStr.length>0){ createModel.errText = @""; } [cell refreshContent:createModel formModel:_shopFormModel]; return cell; } else if(createModel.cellType == CreateShopCellType_PIC) { static NSString *picCellId = @"picCellId"; CreateShopUploadPicCell *cell = [tableView dequeueReusableCellWithIdentifier:picCellId]; if(cell==nil) { cell = [[CreateShopUploadPicCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:picCellId]; } id value = [_shopFormModel valueForKey:createModel.key]; if([value isKindOfClass:[NSString class]]){ NSString *valueStr = (NSString *)value; if(valueStr.length>0){ createModel.errText = @""; } } else if([value isKindOfClass:[UIImage class]]){ UIImage *valueImg = (UIImage *)value; if{ createModel.errText = @""; } } __weak CreateShopViewController *weakSelf = self; [cell refreshContent:createModel formModel:_shopFormModel editBlock:^(CreateShopModel *shop) { if  { _shopFormModel.indexPath = indexPath; _shopFormModel.indexPathObj = shop; [weakSelf iconActionSheet]; } }]; return cell; } return nil;}

首先比较简单的,在设置行高的代理方法里,根据该行数据所表示的cellType类型来设置相应的行高。然后在返回cell的代理方法里,同样以cellType来判断返回相应样式的cell,并给该cell赋相应的数据model。但是我们注意到,给cell赋值的方法,除了传入我们前面说定义的CreateShopModel类型的createModel外,还有个名叫_shopFormModel参数被传入。_shopFormModel是什么,它代表什么意思?

_shopFormModelCreateShopFormModel类型的一个实例对象,它用来表示这个表单需要提交的数据,它里面的每个属性基本上对应着表单提交给服务器的字段。我们最后不是要将表单数据作为参数去请求提交的接口吗?表单数据从哪里来,就从_shopFormModel中来。那_shopFormModel中的数据从哪里来?

#import <Foundation/Foundation.h>@interface CreateShopFormModel : NSObject@property (nonatomic, copy)NSString *groupId;@property (nonatomic, copy)NSString *groupName;@property (nonatomic, copy)NSString *tag;@property (nonatomic, copy)NSString *introduction;@property (nonatomic, copy)NSString *regionId;@property (nonatomic, copy)NSString *cityId;@property (nonatomic, copy)NSString *address;@property (nonatomic, copy)NSString *telephone;@property (nonatomic, copy)NSString *contactMail;@property (nonatomic, copy)NSString *coverUrl;@property (nonatomic, copy)NSString *logoUrl;@property (nonatomic, strong)UIImage *logo;@property (nonatomic, strong)UIImage *cover;@property (nonatomic, strong)NSIndexPath *indexPath;@property (nonatomic, strong)id indexPathObj;+ (CreateShopFormModel *)formModelFromDict:(NSDictionary *)dict;-submitCheck:dataArr;@end

CreateShopTFCell为例,它所表示的字段的数据是我们在输入框输入的,也就是说数据来自textField_shopFormModel对象在控制器被传入cellrefreshContent:formModel:方法,在该方法内部,将参数formModel赋给成员变量_formModel需要格外注意的是,_shopFormModelformModel_ formModel是同一个对象,指向的是同一块内存地址。方法传递对象参数时只是“引用拷贝”,拷贝了一份对象的引用。既然这样,我们可以预想到,我们在cell内部,将textField输入的值赋给_formModel所指向的对象后,也即意味着控制器里的_shopFormModel也有数据了,因为它们本来就是同一个对象嘛!

事实正是如此。可以看到我们在给textField添加的通知的回调方法textFiledEditChanged:里,将textField输入的值以KVC的方式赋值给了_formModel。此时_formModel的某属性,即该cell对应的表单的字段已经有了数据。同样的,在控制器中与_formModel指向同一块内存地址的_shopFormModel也有了数据。

- clearCellData{ _titleLab.text = @""; _textField.text = @""; _textField.placeholder = @""; _checkTipLab.text = @"";}- refreshContent:(CreateShopModel *)createModel formModel:(CreateShopFormModel *)formModel{ [self clearCellData]; if(!createModel){ return; } _createModel = createModel; _formModel = formModel; _titleLab.text = createModel.title; _textField.placeholder = createModel.placeholder; _textField.text = [_formModel valueForKey:createModel.key]; // 将_formModel的值以KVC的方式赋给textField if(createModel.errText.length>0){ _bgView.layer.borderColor = HexColor.CGColor; _checkTipLab.text = createModel.errText; }else{ _bgView.layer.borderColor = PDColor_Dividing_line.CGColor; _checkTipLab.text = @""; }}- textFiledEditChanged:(NSNotification *)obj{ UITextField *textField = (UITextField *)obj.object; NSString *toBeString = textField.text; [_formModel setValue:textField.text forKey:_createModel.key]; // 将textField中的值赋给_formModel if(_createModel.maxInputLength&&toBeString.length>0&&toBeString.length>_createModel.maxInputLength){ _bgView.layer.borderColor = HexColor.CGColor; _checkTipLab.text = [NSString stringWithFormat:@"最多%d个字",_createModel.maxInputLength]; }else{ _bgView.layer.borderColor = PDColor_Dividing_line.CGColor; _checkTipLab.text = @""; } if([_createModel.key isEqualToString:@"contactMail"]){ _createModel.errText = @""; }else{ NSString *valueStr = [_formModel valueForKey:_createModel.key]; if(valueStr.length>0){ _createModel.errText = @""; } }}

我们看到在refreshContent:formModel:方法中,cell上的死数据是被CreateShopModel的实例对象createModel赋值的,而在其后我们又以KVC的方式又将_shopFormModel的某属性的值赋给了textField。这是因为我们为了防止cell在复用的过程中出现数据错乱的问题,而在给cell赋值前先将每个视图上的数据都清空了(即clearCellData方法),需要我们重新赋过。(不过,如果你没清空数据的情况下,不再次给textField赋值好像也是没问题的。不会出现数据错乱和滑出屏幕再滑回来时从复用池取出cell后赋值时数据消失的问题。)

/** 移除 键盘 管理器 */+ removeKeyboardHelper;/** 更新 键盘 和 响应者 间距 @param spacing 键盘 和 响应者 间距 */+ updateKeyboardTofirstResponderSpacing:spacing;/** 处理 containerView 键盘 遮挡 @param containerView 需要移动的视图 */+ handleKeyboardWithContainerView:containerView;/** 处理 scrollView 键盘 遮挡 @param scrollView scrollView */+ handleKeyboardWithScrollView:(UIScrollView *)scrollView;/** 处理 键盘 @param showBlock 显示 回调 @param hideBlock 隐藏 回调 */+ handleKeyboardWithShowBlock:(FJFKeyboardManagerBlock)showBlock hideBlock:(FJFKeyboardManagerBlock)hideBlock;
文件夹拖入

在IQKeyboardManager下载项目,然后把IQKeyboardManager文件夹拖入工程:

图片 3文件夹拖入.png做了之后什么代码都不用写IQKeyboardManager就自动启动了,原理是因为IQKeyboardManager.m中重写了+load如果你不需要弹出键盘(你没有加toolbar的键盘)时附带IQKeyboardManager给你配置的toolbar,你可以在你配置文件中写上:

[IQKeyboardManager sharedManager].enableAutoToolbar = NO;

如果你需要在键盘弹出时能都点击键盘之外部分收起键盘,你可以在你配置文件中写上:

[IQKeyboardManager sharedManager].shouldResignOnTouchOutside = YES;

如果你需要在某个控制器禁用IQKeyboardManager,你可以在对应控制器中写上:

- viewWillAppear:animated { [super viewWillAppear:animated]; [[IQKeyboardManager sharedManager] setEnable:NO];}- viewWillDisappear:animated { [super viewWillDisappear:animated]; [[IQKeyboardManager sharedManager] setEnable:YES];}

IQKeyboardManager.m重写了+load,里面启动了IQKeyboardManager:

[[IQKeyboardManager sharedManager] setEnable:YES];

现在我们来追踪sharedManager:

+ (IQKeyboardManager*)sharedManager{ static IQKeyboardManager *kbManager; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ kbManager = [[self alloc] init]; }); return kbManager;}

这是一个常见的单例写法,主要看看init:

__weak typeof weakSelf = self;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{ __strong typeof strongSelf = weakSelf; ...});

这里最开始看我也觉得很迷惑,为什么这里还要写一个dispatch_once而且里面要强引用weakSelf,答案在这里,主要是多线程环境下防止self对象在后面的代码中被析构(析构:第一次是在C++中听说,其实就是被释放了)

[[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];

这里是注册键盘弹出/收起的通知,很好理解

strongSelf.registeredClasses = [[NSMutableSet alloc] init];[self registerTextFieldViewClass:[UITextField class] didBeginEditingNotificationName:UITextFieldTextDidBeginEditingNotification didEndEditingNotificationName:UITextFieldTextDidEndEditingNotification];[self registerTextFieldViewClass:[UITextView class] didBeginEditingNotificationName:UITextViewTextDidBeginEditingNotification didEndEditingNotificationName:UITextViewTextDidEndEditingNotification];

这里要配合[self registerTextFieldViewClass:::]一起看:

-registerTextFieldViewClass:(nonnull Class)aClass didBeginEditingNotificationName:(nonnull NSString *)didBeginEditingNotificationName didEndEditingNotificationName:(nonnull NSString *)didEndEditingNotificationName{ [_registeredClasses addObject:aClass]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidBeginEditing:) name:didBeginEditingNotificationName object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textFieldViewDidEndEditing:) name:didEndEditingNotificationName object:nil];}

可以得知目前registeredClasses只有UITextView和UITextField两个值,也就是文章前面提到的输入源了(也只有这两个控件可以弹出键盘);所以上面的代码是在注册哪些输入源需要解决键盘遮挡,并为其加上编辑的通知从这里可以看到作者这样写是为了方便扩展,比如以后苹果新出了一个输入源只需要再调用一次[self registerTextFieldViewClass:::]即可,而且把所有的输入源编辑通知放到同一个回调中处理也是为了问题集中处理,方便以后修改和检查问题(而且看源码也知道这样做确实有很大好处)

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(willChangeStatusBarOrientation:) name:UIApplicationWillChangeStatusBarOrientationNotification object:[UIApplication sharedApplication]];[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didChangeStatusBarFrame:) name:UIApplicationDidChangeStatusBarFrameNotification object:[UIApplication sharedApplication]];

加上屏幕旋转和状态栏frame改变的通知,因为要重新计算位置等信息;开始还想了一下状态栏frame什么时候会改变?后面想了想屏幕旋转后状态栏frame肯定会变啊,然后暂时想不起其他情况了

strongSelf.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRecognized:)];strongSelf.tapGesture.cancelsTouchesInView = NO; [strongSelf.tapGesture setDelegate:self]; strongSelf.tapGesture.enabled = strongSelf.shouldResignOnTouchOutside;

这是初始化了一个单击手势(只是初始化出来,后面后根据输入源来添加到window上),在键盘弹起来后可以点击键盘外部分收起键盘,通过设置[IQKeyboardManager sharedManager].shouldResignOnTouchOutside = YES/NO开启和关闭此功能;然后cancelsTouchesInView设置为no可以看这里

strongSelf.animationDuration = 0.25;

需要让屏幕滚动才能让输入源可见时,设置滚动动画执行的时间

strongSelf.animationCurve = UIViewAnimationCurveEaseInOut;

需要让屏幕滚动才能让输入源可见时,设置滚动动画的方式

[self setKeyboardDistanceFromTextField:10.0];

设置输入源底部距离键盘顶部的距离,默认为10,你可以改成100然后运行一下看看效果

[self setShouldPlayInputClicks:YES];

用到shouldPlayInputClicks属性的地方是用户点击IQKeyboardManager自带的toolbar中的上一个、下一个和完成按钮时,如果shouldPlayInputClicks为YES将执行如下代码:

[[UIDevice currentDevice] playInputClick]

这句话的作用是播放按键声音

[self setShouldResignOnTouchOutside:NO];

设置默认情况下点击键盘外面部分不收起键盘

[self setOverrideKeyboardAppearance:NO];[self setKeyboardAppearance:UIKeyboardAppearanceDefault];

是否让IQKeyboardManager覆盖用户设置的输入源弹出键盘样式,并设置默认键盘样式

[self setEnableAutoToolbar:YES];

默认让没有toolbar的键盘使用默认的toolbar(如果用户自定义了toolbar就忽略,这点后面的源码我们会讲到)

[self setPreventShowingBottomBlankSpace:NO];

这个属性是为了解决一个bug,原因是因为滚动高度计算错误(具体原因我们后面源码来模拟),滚动过高导致界面和键盘之间有黑色区域;

[self setShouldShowTextFieldPlaceholder:YES];

也就是toolbar中间的文字是否在输入源是UITextField时显示UITextField的占位字符,看一下图就明白了:

图片 4效果.png

[self setToolbarManageBehaviour:IQAutoToolbarBySubviews];

这个属性就比较有意思了,我看了一下用到toolbarManageBehaviour的地方,大致得到意思是:自带的toolbar上有上一个/下一个按钮,那么IQKeyboardManager怎么知道我下一个/上一个应该跳到哪个输入源呢?里面有这个函数来获取所有可以跳的输入源:

-responderViews

那么他们跳转的先后顺序呢?我们就可以对照toolbarManageBehaviour枚举值来看:

IQAutoToolbarBySubviews:按照添加的先后顺序IQAutoToolbarByTag:按照tag值大小IQAutoToolbarByPosition:按照视图在界面上的位置

[self setLayoutIfNeededOnUpdate:NO];

咋一看,为什么要设置成NO呢?我改成了YES然后得到下面的gif图:

图片 5效果图.gif

有一个明显的不协调因为toolbar出现时会再计算一次高度,所以屏幕滚动了两次;所以这个值默认是NO,表示我动画更新界面的frame时不需要再执行一次layout(从源码中可以看出作者最开始是执行了的,后面出现了bug才关闭执行,这也是一种版本迭代的方法,能让使用者更明白问题的原因)

[self setShouldFixInteractivePopGestureRecognizer:YES];

这也是为了解决一个bug,具体为什么我们后面来讨论

strongSelf.disabledDistanceHandlingClasses = [[NSMutableSet alloc] initWithObjects:[UITableViewController class], nil];strongSelf.enabledDistanceHandlingClasses = [[NSMutableSet alloc] init];

强制禁用/开启IQKeyboardManager的控制器,即便设置了enable

strongSelf.disabledToolbarClasses = [[NSMutableSet alloc] init];strongSelf.enabledToolbarClasses = [[NSMutableSet alloc] init];

强制禁用/开启带toolbar键盘的控制器

strongSelf.toolbarPreviousNextAllowedClasses = [[NSMutableSet alloc] initWithObjects:[UITableView class],[UICollectionView class],[IQPreviousNextView class], nil];

前面提到toolbar上面的上一个/下一个该往哪里跳的顺序,需要用到toolbarManageBehaviour枚举值来决定,但是如果输入源的父类是toolbarPreviousNextAllowedClasses中的某一个则强制使用IQAutoToolbarBySubviews这个toolbarManageBehaviour枚举值;因为用其他的枚举值要出现一个bug说的是太复杂(看来做一个库真的是抠脑壳)

strongSelf.disabledTouchResignedClasses = [[NSMutableSet alloc] init]; strongSelf.enabledTouchResignedClasses = [[NSMutableSet alloc] init];

强制开启/禁用点击键盘外面收起键盘

 [self setShouldToolbarUsesTextFieldTintColor:NO];

是否让toolbar上面的上一个/下一个/完成按钮颜色使用输入源的tintColor;设置为NO就是黑色,我设置成YES截了一个图:

图片 6设置成YES的效果图.png

到这里我们初始化就走完了,接下来我们就在测试界面(我工程中的一个界面)中点击一下输入框让其弹出键盘,我们来看看IQKeyboardManager都做了哪些事情,先来看看我来做测试的界面:

图片 7做测试的界面.png这是一个UINavigationController中的UIViewController,self.view加了一个view铺满屏幕,这个view加了一个UITableView铺满view,UITableView的第一行(一个UITextField)、地点行(一个UITextField)和详情说明行(一个UITextView)是可以作为输入源弹出键盘的,我们接下来就点击地点行来做测试;前面源码提到键盘弹出/消失和输入源编辑都是有通知的,我们在这些回调中都打上断点

我大概想了一下,测试无非就两种情况1:点击某个输入源第一次弹起键盘2:在键盘已经弹起的时候切换输入源,这里要分两种情况1)用户自己点击下一个输入源2)用户使用toolbar的上一个/下一个按钮由IQKeyboardManager切换到下一个输入源所以接下来我们就来测试;我们点击上一系列提到的测试界面中的地点行,分析各个逻辑

当点击地点行中的UITextField时,首先进入输入框已经开始编辑的通知:

-textFieldViewDidBeginEditing:(NSNotification*)notification()

我们来分析里面的代码:

[self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector]];

_cmd表示当前执行的指令(也不太知道用什么词语恰当)名字,showLog是作者自己写的方法:

-showLog:(NSString*)logString{ if (_enableDebugging) { NSLog(@"IQKeyboardManager: %@",logString); }}

这样做了一个判断,来决定是否需要在终端打印当前执行的指令名称;而且这样做也有一个好处,如果用户需要把执行过的所有指令写入到文件,那么直接在这里写就好了

_textFieldView = notification.object;

把当前的输入源用_textFieldView保存起来;textFieldView是UIView类型,因为前面说过作者是把所有的输入源编辑通知在同一个回调处理

if (_overrideKeyboardAppearance == YES){ UITextField *textField = (UITextField*)_textFieldView; if (textField.keyboardAppearance != _keyboardAppearance){ textField.keyboardAppearance = _keyboardAppearance; [textField reloadInputViews]; } }

是否让IQKeyboardManager覆盖输入源的键盘样式;keyboardAppearance是UITextInputTraits协议中方法,而UITextField和UITextView都实现了UITextInput协议,UITextInput实现了UIKeyInput协议,UIKeyInput实现了UITextInputTraits协议:

@protocol UIKeyInput <UITextInputTraits>

if ([self privateIsEnableAutoToolbar])-privateIsEnableAutoToolbar{ BOOL enableAutoToolbar = _enableAutoToolbar; UIViewController *textFieldViewController = [_textFieldView viewController]; ... return enableAutoToolbar;}

[_textFieldView viewController]是作者为UIView加的一个分类,用来获取输入源所在的控制器:

-(UIViewController*)viewController{ UIResponder *nextResponder = self; do { nextResponder = [nextResponder nextResponder]; if ([nextResponder isKindOfClass:[UIViewController class]]) return (UIViewController*)nextResponder; } while (nextResponder != nil); return nil;}

判断当前输入源所在的控制器是否应许弹出toolbar,因为我设置了整个工程禁用此属性:

[IQKeyboardManager sharedManager].enableAutoToolbar = NO;

所以返回NO(你也可以进去看看,里面用到了强制使用(_disabledToolbarClasses)和强制启动toolbar(_enabledToolbarClasses)),然后到了:

[self removeToolbarIfRequired];-removeToolbarIfRequired{ NSArray *siblings = [self responderViews]; for (UITextField *textField in siblings) { UIView *toolbar = [textField inputAccessoryView]; if ([textField respondsToSelector:@selector(setInputAccessoryView:)] && ([toolbar isKindOfClass:[IQToolbar class]] && (toolbar.tag == kIQDoneButtonToolbarTag || toolbar.tag == kIQPreviousNextButtonToolbarTag))) { textField.inputAccessoryView = nil; } }}-responderViews{ UIView *superConsideredView; for (Class consideredClass in _toolbarPreviousNextAllowedClasses){ superConsideredView = [_textFieldView superviewOfClassType:consideredClass]; if (superConsideredView != nil) break; } if (superConsideredView) { return [superConsideredView deepResponderViews]; } else { NSArray *textFields = [_textFieldView responderSiblings]; switch (_toolbarManageBehaviour) { case IQAutoToolbarBySubviews: return textFields; break; case IQAutoToolbarByTag: return [textFields sortedArrayByTag]; break; case IQAutoToolbarByPosition: return [textFields sortedArrayByPosition]; break; default: return nil; break; } }}

判断是否需要删除IQKeyboardManager附带的toolbar,这里要注意的是responderViews中的_toolbarPreviousNextAllowedClasses,上一个系列我们说到_toolbarPreviousNextAllowedClasses目前有三个值UITableView、UICollectionView和IQPreviousNextView;如果输入源是在这三个之中,将采用另外一种方式来获取所有能够弹起键盘的输入源,所以针对本测试界面将得到下面的输入:

 po [self responderViews]<__NSArrayM 0x618000053230>(<UITextField: 0x7fca2b69a860; frame = (15 5; 300 39.5); text = '领导弟弟的和'; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x610000258e10>; layer = <CALayer: 0x610000429240>>,<UITextField: 0x7fca2b730fd0; frame = (15 5; 290 39.5); text = ''; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x60000044bb50>; layer = <CALayer: 0x6080004257c0>>,<UITextView: 0x7fca2fa21800; frame = (15 5; 290 89.5); text = ''; clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x60000044e8e0>; layer = <CALayer: 0x60000023ba80>; contentOffset: {0, 0}; contentSize: {290, 81}>)

然后IQKeyboardManager也只会删除自己本身创建的toolbar(通过[toolbar isKindOfClass:[IQToolbar class]就可以看出),然后继续执行到了:

if ([self privateIsEnabled] == NO){ [self showLog:[NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector]]; return; }-privateIsEnabled{ BOOL enable = _enable; UIViewController *textFieldViewController = [_textFieldView viewController]; if (textFieldViewController) ... return enable;}

这里是判断当前控制器是否启动了IQKeyboardManager,原理和上面是否开启toolbar是一样的,看了这里,我们是不是应该用新方式来禁用某个控制器启动IQKeyboardManager呢,你只需要在配置文件中写上:

...[[IQKeyboardManager sharedManager].disabledDistanceHandlingClasses addObject:[XXXXXX class]];...

测试界面返回YES,所以继续执行到了:

[_textFieldView.window addGestureRecognizer:_tapGesture];

还记得上一系列我们说到的吗?单击手势只是创建了,这里才加到输入源所在的window上,然后到了这个函数:

if (CGRectEqualToRect(_topViewBeginRect, CGRectZero)){...}

我们现在是第一次遇到_topViewBeginRect,这主要是解决UITextField和UITextView触发键盘通知和编辑通知先后不同的bug,所以这个条件为真,我们进入函数中执行:

_layoutGuideConstraintInitialConstant = [[[_textFieldView viewController] IQLayoutGuideConstraint] constant];

这里要注意IQLayoutGuideConstraint是什么东西,它是给UIViewController添加的属性,我们先想一下我们测试界面有添加这个吗?答案是:

 po [[_textFieldView viewController] IQLayoutGuideConstraint] nil

因为我们是代码创建的,作者添加这个是因为一个bug;我们后面会讲到,所以测试界面得到的_layoutGuideConstraintInitialConstant也是为0:

 po _layoutGuideConstraintInitialConstant0

然后到了:

_rootViewController = [_textFieldView topMostController];if (_rootViewController == nil) _rootViewController = [[self keyWindow] topMostController];#UIView (IQ_UIView_Hierarchy)-(UIViewController *)topMostController{ NSMutableArray *controllersHierarchy = [[NSMutableArray alloc] init]; UIViewController *topController = self.window.rootViewController; if (topController){ [controllersHierarchy addObject:topController]; } while ([topController presentedViewController]) { topController = [topController presentedViewController]; [controllersHierarchy addObject:topController]; } UIResponder *matchController = [self viewController]; while (matchController != nil && [controllersHierarchy containsObject:matchController] == NO){ do{ matchController = [matchController nextResponder]; } while (matchController != nil && [matchController isKindOfClass:[UIViewController class]] == NO); } return (UIViewController*)matchController;}#UIWindow (IQ_UIWindow_Hierarchy)- (UIViewController*)topMostController{ UIViewController *topController = [self rootViewController]; while ([topController presentedViewController]) topController = [topController presentedViewController]; return topController;}

这里就不需要解释的太复杂了,就是通过输入源响应链得到工程最顶层的控制器(其中要注意presentedViewController的理解),为了让你们更深刻的理解,我做了一个小实验,我在topMostController函数中的while前加一个假数据:

...UIViewController *vc = [UIViewController new];[self.window.rootViewController presentViewController:vc animated:NO completion:nil];while ([topController presentedViewController]) {...

然后得到的数组是:

 po controllersHierarchy<__NSArrayM 0x61800024c630>(<MainViewController: 0x7fa8781091e0>,<UIViewController: 0x7fa875c66ab0>)

最后_rootViewController的值是:

 po matchController<MainViewController: 0x7fa8781091e0>

因为我在Appdelegate中写了(一般工程得到的都是self.window.rootViewController):

self.window.rootViewController = [[MainViewController alloc] initWithOptions:launchOptions];

继续执行代码,接下来是:

_topViewBeginRect = _rootViewController.view.frame

把顶层控制器的frame赋给_topViewBeginRect至于要做啥我们后面就会看到,我得到的是:

 po _topViewBeginRect(origin = (x = 0, y = 0), size = (width = 320, height = 568))

接下来是如果顶层控制器是一个导航视图控制器将导致左滑返回实效的bug,需要重新计算一下frame:

if (_shouldFixInteractivePopGestureRecognizer && [_rootViewController isKindOfClass:[UINavigationController class]]){ _topViewBeginRect.origin = CGPointMake(0, [self keyWindow].frame.size.height-_rootViewController.view.frame.size.height);}[self showLog:[NSString stringWithFormat:@"Saving %@ beginning Frame: %@",[_rootViewController _IQDescription], NSStringFromCGRect(_topViewBeginRect)]];

然后执行到了:

if (_isKeyboardShowing == YES && _textFieldView != nil && [_textFieldView isAlertViewTextField] == NO){ [self adjustFrame];}[self showLog:[NSString stringWithFormat:@"****** %@ ended ******",NSStringFromSelector]];

这里判断_isKeyboardShowing是因为UITextField和UITextView触发键盘通知和编辑通知先后不同,判断_textFieldView是防御性编程思想,判断isAlertViewTextField是因为一个bug(如果是UIAlertView里面的输入框,苹果已经帮你做好了键盘遮挡的处理;到了这里我们知道有两个地方是不需要解决键盘遮挡输入源的:UIAlertView里面的输入源和UITableViewController里面的输入源)目前_isKeyboardShowing为NO,所以这个通知我们就执行完了;然后进入了键盘将要显示的通知:

-keyboardWillShow:(NSNotification*)aNotification{}

我们来分析一下逻辑,先是把通知赋给_kbShowNotification,至于什么用我们后面才知道:

_kbShowNotification = aNotification;

然后把键盘弹起的标志设置为YES,常规的判断当前控制器是否可用,然后判断_topViewBeginRect是否没有值,我们现在有了就不会执行了(主要还是因为UITextField和UITextView触发键盘通知和编辑通知先后不同做了很多的重复代码,记住!记住!!记住!!!):

_isKeyboardShowing = YES;if ([self privateIsEnabled] == NO) return;[self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector]];if (_textFieldView != nil && CGRectEqualToRect(_topViewBeginRect, CGRectZero)) {...}

然后执行到了:

_animationCurve = [[aNotification userInfo][UIKeyboardAnimationCurveUserInfoKey] integerValue];_animationCurve = _animationCurve<<16;CGFloat duration = [[aNotification userInfo][UIKeyboardAnimationDurationUserInfoKey] floatValue];if (duration != 0.0) _animationDuration = duration;

得到键盘弹出来的动画方式,保存动画执行的时间,时间我们初始化的时候设置成了0.25,这里如果键盘有动画时间我们会重新获取一次更准确一点:

CGSize oldKBSize = _kbSize;CGRect kbFrame = [[aNotification userInfo][UIKeyboardFrameEndUserInfoKey] CGRectValue];CGRect screenSize = [[UIScreen mainScreen] bounds];

oldKBSize主要是用在切换输入源的时候保持前一个的键盘frame,获取当前键盘的frame保存到kbFrame,获取屏幕frame保存到screenSize:

CGRect intersectRect = CGRectIntersection(kbFrame, screenSize);if (CGRectIsNull(intersectRect)) { _kbSize = CGSizeMake(screenSize.size.width, 0);} else { _kbSize = intersectRect.size;}

这也是一个bug我也遇到过,就是键盘没有完全的弹出来,所以作者机智的用CGRectIntersection()得到键盘和屏幕重叠的部分作为键盘的frame保存到_kbSize:

if (!CGSizeEqualToSize(_kbSize, oldKBSize)){ if (_isKeyboardShowing == YES && _textFieldView != nil && [_textFieldView isAlertViewTextField] == NO){ [self adjustFrame]; } }

判断上一个输入源键盘的frame和现在的是不是一样,我们这里是不一样的因为我们现在是第一次弹出键盘:

 po _kbSize(width = 320, height = 253) po oldKBSize(width = 0, height = 0)

然后就是这个重点函数了,一共420+行:

-adjustFrame{...}

我打算单独其一个系列来说,因为这个函数才是IQKeyboardManager的核心,其他的很多变量和操作都只是为了解决bug和定制功能而已

if (_textFieldView == nil) return;[self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector]];//得到window对象UIWindow *keyWindow = [self keyWindow];

首先还是一个常规的判断和日志打印,以及得到window对象

UIViewController *rootController = [_textFieldView topMostController]; if (rootController == nil) rootController = [keyWindow topMostController];

这个前面已经说到了,是获取顶层控制器

CGRect textFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];CGRect rootViewRect = [[rootController view] frame];

得到输入框在屏幕中的位置已经屏幕的位置,我么打印一下看看:

 po textFieldViewRect(origin = (x = 15, y = 357.00999999977648), size = (width = 290, height = 39.5)) po rootViewRect(origin = (x = 0, y = 0), size = (width = 320, height = 568))

下面做了这么多,其实都是在计算输入源底部应该在键盘上面多少距离

CGFloat specialKeyboardDistanceFromTextField = _textFieldView.keyboardDistanceFromTextField;if (_textFieldView.isSearchBarTextField) { UISearchBar *searchBar = (UISearchBar*)[_textFieldView superviewOfClassType:[UISearchBar class]]; specialKeyboardDistanceFromTextField = searchBar.keyboardDistanceFromTextField;}CGFloat keyboardDistanceFromTextField = (specialKeyboardDistanceFromTextField == kIQUseDefaultKeyboardDistance)?_keyboardDistanceFromTextField:specialKeyboardDistanceFromTextField;

我们打印一下,也就是我们在init中设置的10:

 po keyboardDistanceFromTextField10

得到键盘高度+ keyboardDistanceFromTextField的高度(也就是屏幕在输入源下面有多少高度),以及状态栏高度:

CGSize kbSize = _kbSize;kbSize.height += keyboardDistanceFromTextField;//状态栏frameCGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];

我们还是打印一下:

 po statusBarFrame(origin = (x = 0, y = 0), size = (width = 320, height = 20)) po kbSize.height263

下面做了这么多都是因为一个bug,最终目的是得到应该让视图往上移动多少:

IQLayoutGuidePosition layoutGuidePosition = IQLayoutGuidePositionNone; NSLayoutConstraint *constraint = [[_textFieldView viewController] IQLayoutGuideConstraint]; //http://blog.kyleduo.com/2014/10/22/ios_learning_autolayout_toplayoutguide/ if (constraint.firstItem == [[_textFieldView viewController] topLayoutGuide] || constraint.secondItem == [[_textFieldView viewController] topLayoutGuide]){ layoutGuidePosition = IQLayoutGuidePositionTop; } else if (constraint.firstItem == [[_textFieldView viewController] bottomLayoutGuide] || constraint.secondItem == [[_textFieldView viewController] bottomLayoutGuide]){ layoutGuidePosition = IQLayoutGuidePositionBottom; } CGFloat topLayoutGuide = CGRectGetHeight(statusBarFrame); CGFloat move = 0; if (layoutGuidePosition == IQLayoutGuidePositionBottom){ move = CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-kbSize.height); } else{ move = MIN(CGRectGetMinY(textFieldViewRect)-(topLayoutGuide+5), CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-kbSize.height)); }[self showLog:[NSString stringWithFormat:@"Need to move: %.2f",move]];

这个bug讲到导致,如果控制器的view添加了topLayoutGuide或者bottomLayoutGuide将会失效,也就是这个

图片 8图.png

不过我们一般都不会用这个约束,所以我打印一下我应该移动多少:

 po move91.509999999776483

下面是找到能够滚动的当前输入框的父亲视图superScrollView:

UIScrollView *superScrollView = nil;UIScrollView *superView = (UIScrollView*)[_textFieldView superviewOfClassType:[UIScrollView class]]; //得到能够滚动的滚动视图 上面找到的可能不能滚动 while (superView) { if (superView.isScrollEnabled) { superScrollView = superView; break; } else { superView = (UIScrollView*)[superView superviewOfClassType:[UIScrollView class]]; } }

通过这个代码我们就可以解决一个整个window都上移动的问题,我们只需要把输入源的某一个父视图设置为滚动视图即可;我这里打印出来的当然是表格视图了:

 po superView<UITableView: 0x7fe6d005fc00; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x608000458f00>; layer = <CALayer: 0x60800003cea0>; contentOffset: {0, 0}; contentSize: {320, 438.00999999977648}> po superScrollView<UITableView: 0x7fe6d005fc00; frame = (0 0; 320 568); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x608000458f00>; layer = <CALayer: 0x60800003cea0>; contentOffset: {0, 0}; contentSize: {320, 438.00999999977648}>

_lastScrollView这个属性我们还是第一次遇到,所以会执行else if(superScrollView)里面的代码:

_lastScrollView = superScrollView;//得到需要设置偏移量的滚动视图的各种内容属性_startingContentInsets = superScrollView.contentInset;_startingContentOffset = superScrollView.contentOffset;_startingScrollIndicatorInsets = superScrollView.scrollIndicatorInsets;[self showLog:[NSString stringWithFormat:@"Saving %@ contentInset: %@ and contentOffset : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]];

这里当然就是常规的操作了,把滚动视图赋值给_lastScrollView,获取滚动视图的一些属性以及打印日志

while (superScrollView && (move>0?(move > (-superScrollView.contentOffset.y-superScrollView.contentInset.top)):superScrollView.contentOffset.y>0) ) {...}

这个while就是先找到一个父滚动视图,判断它能不能把所有的move都给偏移完,如果父滚动视图偏移达到最大了move还是大于0,就继续找父滚动视图的父滚动视图,依次类推;需要注意里面的这段代码:

if ([_textFieldView isKindOfClass:[UITextView class]] &&[superScrollView superviewOfClassType:[UIScrollView class]] == nil && (shouldOffsetY >= 0)) {...}

如果输入源是UITextView,并且输入源的父类都没有发现滚动视图,并且当前还需要滚动(shouldOffsetY表示界面还需要上/下移动多少,才能让键盘不遮挡输入源);就不需要继续递归查找滚动视图了,因为没有可以移动的视图,只需要重新计算偏移多少结束while循环

[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{... superScrollView.contentOffset = CGPointMake(superScrollView.contentOffset.x, shouldOffsetY);} completion:NULL];

这里就是对输入源的父滚动视图进行偏移

lastView = superScrollView;superScrollView = (UIScrollView*)[lastView superviewOfClassType:[UIScrollView class]];

偏移了当前滚动视图,我们继续偏移滚动视图的父滚动视图(如果有父滚动视图且move>0);当我们偏移完了(move=0或move>0且没有父滚动视图可以偏移)我们就进入到更新滚动视图contentInset的代码:

CGRect lastScrollViewRect = [[_lastScrollView superview] convertRect:_lastScrollView.frame toView:keyWindow];

得到输入源父滚动视图在window中的frame,我们打印一下值:

 po lastScrollViewRect(origin = (x = 0, y = 64), size = (width = 320, height = 568))

CGFloat bottom = kbSize.height-keyboardDistanceFromTextField-(CGRectGetHeight(keyWindow.frame)-CGRectGetMaxY(lastScrollViewRect));

得到输入源底部距屏幕底部的距离,打印出来是317

UIEdgeInsets movedInsets = _lastScrollView.contentInset;movedInsets.bottom = MAX(_startingContentInsets.bottom, bottom);

重新计算一下滚动视图的内容偏移contentInset,一般情况是不变的,只是在滚动视图底部进行偏移时会出问题,下面就是动画设置还有打印了前面说到如果对控制器设置了TopLayoutGuide或者BottomLayoutGuide时会出问题,所以接下来就是对它们进行单独处理获取正确的约束值,因为我们当前界面没有设置,所以我们会运行else;

if ([_textFieldView isKindOfClass:[UITextView class]]) {...}

这个if是解决如果UITextView在屏幕中frame太大的问题

UITextView *textView = (UITextView*)_textFieldView; CGFloat textViewHeight = MIN(CGRectGetHeight(_textFieldView.frame), (CGRectGetHeight(keyWindow.frame)-kbSize.height-(topLayoutGuide)));

textViewHeight为:屏幕高度-键盘高度-TopLayoutGuide约束

if (_textFieldView.frame.size.height-textView.contentInset.bottom>textViewHeight) {...UIEdgeInsets newContentInset = textView.contentInset;newContentInset.bottom = _textFieldView.frame.size.height-textViewHeight;textView.contentInset = newContentInset;textView.scrollIndicatorInsets = newContentInset;...}

如果高度实在太高,我们就设置UITextView的contentInset.bottom让UITextView内容增加时能始终在键盘上面显示,因为contentInset.bottom使得UITextView下面的区域不能输入内容

if ([rootController modalPresentationStyle] == UIModalPresentationFormSheet ||[rootController modalPresentationStyle] == UIModalPresentationPageSheet) {...}

这是判断ipad,我们是iPhone,所以到了这里

if (move>=0) { rootViewRect.origin.y -= move; if (_preventShowingBottomBlankSpace == YES){ rootViewRect.origin.y = MAX(rootViewRect.origin.y, MIN(0, -kbSize.height+keyboardDistanceFromTextField)); } [self showLog:@"Moving Upward"]; [self setRootViewFrame:rootViewRect];}

这里是一个比较精髓的地方,如果滚动视图都没有把move消费完(move还是大于0,也就是输入源还是被键盘遮挡住的) 就需要对window的frame进行设置了,我们来看看else执行了什么

CGFloat disturbDistance = CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect); if(disturbDistance<0){ rootViewRect.origin.y -= MAX(move, disturbDistance); [self showLog:@"Moving Downward"]; [self setRootViewFrame:rootViewRect];}

move<0 说明输入源底部距离键盘顶部还有一定距离,说不定我们可以稍微恢复一下windw的frame(如果widnow的frame被设置过)运行到这里,我们的第一次弹出键盘的情况就完了,我们来试着切换一下输入源看看

我们点击另外一个输入源,会直接进入

-textFieldViewDidBeginEditing:(NSNotification*)notification

输入源将要开始编辑的回调,里面又去执行了一次adjustFrame解决键盘遮挡;然后到了

-keyboardWillShow:(NSNotification*)aNotification {...if (!CGSizeEqualToSize(_kbSize, oldKBSize)) {...}}...

这里就比较有意思了,因为我们当前输入源和上一个输入源是一样的大小:

 po oldKBSize(width = 320, height = 253) po _kbSize(width = 320, height = 253)

所以不会执行adjustFrame,然后到了

- keyboardDidShow:(NSNotification*)aNotification

这个回调几乎都会去再次执行adjustFrame方法,然后点击另外一个输入源回调就执行完了,我们现在消失键盘

我们点击done收起键盘,先进入

- keyboardWillHide:(NSNotification*)aNotification

键盘将要消失的回调,其实这里面是在进行反操作了,就是恢复滚动视图偏移量啥的,如果改变了window的frame就恢复等等;其他的回调我就不说了,所以通过上面的分析我们可以得出结论了

1:IQKeyboardManager在需要解决键盘遮挡时会去递归找可滚动的父视图进行偏移,如果没有就对window的frame做文章2:核心方法是adjustFrame,通过它解决键盘遮挡3:IQKeyboardManager考虑的非常全面,以至于里面很多的判断语句,主要是为了能使用于目前发现的任何情况4:看源码主要是看一个思路,没必要去弄清楚每个小代码块啥意思5:一个好的库最开始是很简单的,到后面慢慢的就会越写越复杂、越全面;所以看源码建议从最初的版本开始,这样才能知道作者为什么要加某些东西,加这些东西是为了什么

输入长度的限制:

需求中要求“网店名称”、“网店主标签”、“网店简介”、“网店座机”都有输入长度的限制,分别为30、20、500、15字数的限制。其实我们在上面初始化数据源的时候已经为每行的数据源model设置过字数限制了,即maxInputLength属性。

我们还是以CreateShopTFCell为例。要在开始输入的时候监听输入的长度,若字数超过最大限制,则要出现红框,并且显示提示信息。那我们就得给textField开始输入时添加valueChange的观察,在textField输入结束时移除观察。

- textFieldDidEndEditing:(UITextField *)textField{ [self clearNotification];}- textFiledEditChanged:(NSNotification *)obj{ UITextField *textField = (UITextField *)obj.object; NSString *toBeString = textField.text; [_formModel setValue:textField.text forKey:_createModel.key]; // 将textField中的值赋给_formModel if(_createModel.maxInputLength&&toBeString.length>0&&toBeString.length>_createModel.maxInputLength){ _bgView.layer.borderColor = HexColor.CGColor; _checkTipLab.text = [NSString stringWithFormat:@"最多%d个字",_createModel.maxInputLength]; }else{ _bgView.layer.borderColor = PDColor_Dividing_line.CGColor; _checkTipLab.text = @""; } if([_createModel.key isEqualToString:@"contactMail"]){ _createModel.errText = @""; }else{ NSString *valueStr = [_formModel valueForKey:_createModel.key]; if(valueStr.length>0){ _createModel.errText = @""; } }}-addNotification{ [[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(textFiledEditChanged:) name:@"UITextFieldTextDidChangeNotification" object:nil];}-clearNotification{ [[NSNotificationCenter defaultCenter]removeObserver:self name:@"UITextFieldTextDidChangeNotification" object:nil];}

另外,可以看到在textField开始输入的回调方法里,调用了该cell的代理方法。该cell为什么要调用这个代理方法,它需要代理给别人来干什么?...其实这个和键盘遮挡的处理有关,下面我们慢慢解释。

举个例子:

处理键盘遮挡问题:

这个界面有很多行输入框,在自然情况下,下面的几个输入框肯定是在键盘弹出后高度之下的,也即会被键盘遮挡住,我们没法输入。这时就一定处理键盘遮挡问题了。关于键盘遮挡问题,其实我在以前的一篇笔记中就写过了:UITextField一箩筐——输入长度限制、自定义placeholder、键盘遮挡问题

我们要处理键盘遮挡问题,也就是要实现当键盘弹出时,被遮挡住的输入框能上移到键盘高度之上;当键盘收回时,输入框又能移回原来的位置。那么首先第一步,我们得能获取到键盘弹出或者收回这个动作的时机,在这个时机我们再按需要移动输入框的位置。系统提供了表示键盘弹出和收回的两个观察的key,分别为UIKeyboardWillShowNotificationUIKeyboardWillHideNotification。注册这两个观察者,然后在两者的回调方法里实现输入框位移就大功告成了。

因为键盘遮挡的处理有可能是比较普遍的需求,所以在公司的项目架构设计里是把上面两个关于键盘的观察是注册在APPDelegate.m中的,并定义了一个有关键盘遮挡处理的协议,协议里定义了一个方法。具体需要具体处理,由需要处理键盘遮挡问题的控制器来实现该协议方法,具体实现怎么移动界面元素来使键盘不遮挡输入框。这么说现在CreateShopViewController控制器需要处理键盘遮挡问题,那么就需要设置它为APPDelegate的代理,并由它实现所定义的协议吗?其实不用,公司项目所有的控制器都是继承于基类CommonViewController,在基类中实现了比较基本和普遍的功能,其实在基类中便定义了下面的方法来设置控制器为APPDelegate的代理,不过需要属性isListensKeyboardYES。下面这个方法在CommonViewController中是在viewWillAppear:方法中调用的。那我们在子类CreateShopViewController中需要做的仅仅只要在viewWillAppear之前设置isListensKeyboard属性为YES,便会自动设置将自己设为APPDelegate的代理。然后在CreateShopViewController控制器里实现协议所定义的方法,实现具体的输入框移动问题。

** CommonViewController.m**

-initListensKeyboardNotificationDelegate{ if (!self.isListensKeyboard) { return; } if (!self.appDelegate) { self.appDelegate=(AppDelegate*)[[UIApplication sharedApplication] delegate]; } [self.appDelegate setKeyboardDelegate:self];}

** CreateShopViewController.m**

#pragma mark - keyboard delegate- keyboardChangeStatus:(KeyboardChangeType)changeType beginFrame:beginFrame endFrame:endFrame duration:duration userInfo:(NSDictionary *)info{ if(changeType == KeyboardWillShow) { CGFloat keyBoard_h = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; CGFloat newSizeh = _tableView.contentSize.height + keyBoard_h; [UIView animateWithDuration:duration animations:^{ [_tableView setContentSize:CGSizeMake(PDWidth_mainScreen, newSizeh)]; CGFloat set_y = _inputY+50.f+keyBoard_h-_tableView.bounds.size.height; if(set_y>0){ [_tableView setContentOffset:CGPointMake]; } }]; } else if(changeType == KeyboardWillHide) { [UIView animateWithDuration:duration animations:^{ [_tableView setContentSize:CGSizeMake(PDWidth_mainScreen, _tableView.contentSize.height)]; }]; }}

可以看到在该代理方法的实现里。当键盘弹出时,我们首先将tableViewcontentSize在原来的基础上增加了键盘的高度keyBoard_h。然后将tableViewcontentOffset值变为set_y,这个set_y的值是通过计算而来,但是计算它的_inputY这个变量代表什么意思?

我们可以回过头去看看tableView返回cell的代理方法中,当为CreateShopTFCell时,我们设置了当前控制器为其cell的代理。

cell.cellDelegate = self;

并且我们的控制器CreateShopViewController也实现了该cell的协议CreateShopTFCellDelegate,并且也实现了协议定义的方法。

#pragma mark - tfCell delegate- cellBeginInputviewY:orginY{ _inputY = orginY;}

原来上面的_intputY变量就是该协议方法从cell里的调用处传递而来的orginY参数值。我们回过头看上面的代码,该协议方法是在textField的开始输入的回调方法里调用的,给协议方法传入的参数是self.frame.origin.y,即被点击的textField在手机屏幕内所在的Y坐标值。

可以看到,处理键盘遮挡问题,其实也不是改变输入框的坐标位置,而是变动tableViewcontentSizecontentOffset属性。