深入讲解iOS键盘二:使用IQKeyboardManager解决键盘遮挡问题

本系列博客是本人的开发笔记。欢迎一起讨论

大家在日常开发中应该知道,表单提交页面是比较难处理的,一是因为UI上需要处理UITextField/UITextView的样式以及键盘的显示隐藏;二是因为在处理接口的时候一般是POST方式需要做各种校验;三是如果提交失败,还要做各种后续诸如弹框等的处理工作。下面笔者要给大家展示的就是一个我们日常开发中可能遇到的信息填写页面,这次我们要实现的效果类似于iMessage中的用户信息编辑页面:


image

可以看到,当我们点击位于屏幕下方的TextField时,弹出键盘,但随之TableView也往上滑动了一下,这样就避免了键盘遮挡输入框的问题。根据上一篇文章的知识点,实现的方式相信大家应该很容易就能想到,无非是监听到键盘弹出时更改TableView的ContentOffset值。这里,笔者做了个简单的Demo,实现的效果如下:


image

代码应该也很好理解:
-(void)viewDidLoad{
    [super viewDidLoad];
    //监听键盘弹出通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyBoardWillShowWithNotification:) name:UIKeyboardWillChangeFrameNotification object:nil];
}
//在Cell中加入UITextField,并设置Delegate为Self
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    static NSString *cellIdentifier = @"cellIdentifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
        UITextField *field = [[UITextField alloc] initWithFrame:CGRectMake(15, 10, SCREEN_WIDTH - 30, 24)];
        field.delegate = self;
        field.placeholder = [NSString stringWithFormat:@"You can input Something At Index %li",(long)indexPath.row];
        [cell.contentView addSubview:field];
    }
    
    return cell;
}
//在即将编辑之前计算出TextField的位置,并转化成TableView中的位置
- (void)textFieldDidBeginEditing:(UITextField *)textField {
    self.activedTextFieldRect = [textField convertRect:textField.frame toView:self.tableview];
}
//当键盘弹出时,动态改变TableView的ContentOffset的值
- (void)keyBoardWillShowWithNotification:(NSNotification *)notification {
    //get FrameEnd
    CGRect rect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
    //get AnimationDuration
    double duration = [notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    //keyboard height > textView height, need scroll
    if ((self.activedTextFieldRect.origin.y + self.activedTextFieldRect.size.height) >  ([UIScreen mainScreen].bounds.size.height - rect.size.height))
    {
        [UIView animateWithDuration:duration animations:^{
            self.tableview.contentOffset = CGPointMake(0, 64 + self.activedTextFieldRect.origin.y + self.activedTextFieldRect.size.height - ([UIScreen mainScreen].bounds.size.height - rect.size.height));
        }];
    }
}

以上代码可以点击这里从github获取到

可以看到,这么处理的一个繁琐的地方是,必须要在TableViewCell中获取到TextField,并将TextField的Delegate设置为当前的控制器。这显然不是一个好的解决方案,那有没有更好的解决方案呢,答案是肯定的,只是大家没想到会有多么简单:
在工程的Podfile中添加如下这句话:

pod 'IQKeyboardManager'

即可。是的,引入IQKeyboardManager库即可。他会自动帮我们解决如上问题,下面我们看一下删除我们刚刚的代码,并加入库IQKeyboardManager后的效果:

image

可以看到我们的键盘上面多了一个小的工具栏,可以选择上下箭头来切换需要填写的TextField,我们的PlaceHolder也显示在了这个小工具栏上。鉴于有些读者可能对这个库的使用不是很熟悉,这里对其使用做个简单的介绍:

IQKeyboardManager的开启/关闭
 [IQKeyboardManager sharedManager].enable = YES;

默认情况下IQKeyboardManager是开启的,如果我们需要针对某个控制器关闭,可以做这样处理:

-(void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [IQKeyboardManager sharedManager].enable = NO;
}

-(void)viewWillDisappear:(BOOL)animated {
    [IQKeyboardManager sharedManager].enable = YES;
    [super viewWillDisappear:animated];
}

或者在 AppDelegate中注册方法:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [[IQKeyboardManager sharedManager] disableInViewControllerClass:[XXXViewController class]];
}
IQKeyboardManager工具条的显示/隐藏
[IQKeyboardManager sharedManager].enableAutoToolbar = NO;
设置点击背景收回键盘
[IQKeyboardManager sharedManager].shouldResignOnTouchOutside = YES;

下面我们来研究一下这个库的源代码。有过一定开发经验的iOS程序员肯定是知道要从load方法开始找起,因为这个库没被引用即可生效,那99%的可能性是使用了这个iOS中的“黑魔法”。
load方法位于类IQKeyboardManager中,实现如下:

+(void)load
{
    //Enabling IQKeyboardManager. Loading asynchronous on main thread
    [self performSelectorOnMainThread:@selector(sharedManager) withObject:nil waitUntilDone:NO];
}

可以看到,load函数中,对IQKeyboardManager这个单例实现了初始化,初始化重载了init方法。这里看到了我们熟悉的注册通知的方法:

[strongSelf registerAllNotifications];

这个方法的实现中,最主要的是监听了UITextFieldTextDidBeginEditingNotification通知

[self registerTextFieldViewClass:[UITextField class]
     didBeginEditingNotificationName:UITextFieldTextDidBeginEditingNotification
       didEndEditingNotificationName:UITextFieldTextDidEndEditingNotification];

因此,当UITextField获取焦点后,在方法

-(void)textFieldViewDidBeginEditing:(NSNotification*)notification

中进行了添加键盘工具栏,调整ScrollView等可能被键盘遮挡的View的相应属性。具体的调整方法位于

-(void)adjustFrame

中。这也是IQKeyboardManager库最重要的方法,也是我们主要分析的方法。

-(void)adjustFrame
{
    UIWindow *keyWindow = [self keyWindow];
    //获取当前控制器
    UIViewController *rootController = [_textFieldView topMostController];
    if (rootController == nil)  rootController = [keyWindow topMostWindowController];
    //获取TextField(或TextView)的Frame
    CGRect textFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];
    CGRect rootViewRect = [[rootController view] frame];

    //省略部分代码....
    //获取移动的高度,如果move是正值,说明textField被隐藏,否则则是被显示了
    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));
    }

    //省略部分代码...
    //找到视图中可能存在的ScrollView
    UIScrollView *superView = (UIScrollView*)[_textFieldView superviewOfClassType:[UIScrollView class]];
    while (superView)
    {
        if (superView.isScrollEnabled && superView.shouldIgnoreScrollingAdjustment == NO)
        {
            superScrollView = superView;
            break;
        }
        else
        {
            //  Getting it's superScrollView.   //  (Enhancement ID: #21, #24)
            superView = (UIScrollView*)[superView superviewOfClassType:[UIScrollView class]];
        }
    }
    //以下代码是为了对获取到的ScrollView进行判断是否是当前遮盖键盘的ScrollView
    if (_lastScrollView){
    }{
    }
    //以下是对Top Layout guide做的特殊处理(如果大家不熟悉的话,没关系,大概意思就是做个兼容)
    if (layoutGuidePosition == IQLayoutGuidePositionTop)
    {
    }
    else if (layoutGuidePosition == IQLayoutGuidePositionBottom){
    }
    else//这里是核心的用于处理键盘遮挡的代码
    {
        //对textView可以通过设置ContentInset来达到效果
       if ([_textFieldView isKindOfClass:[UITextView class]])
        {
            UIEdgeInsets newContentInset = textView.contentInset;
            newContentInset.bottom = strongSelf.textFieldView.frame.size.height-textViewHeight;
            textView.contentInset = newContentInset;
            textView.scrollIndicatorInsets = newContentInset;
            strongSelf.isTextViewContentInsetChanged = YES;
        }
        //  这里再对iPad做个特殊处理
        if ([rootController modalPresentationStyle] == UIModalPresentationFormSheet ||
            [rootController modalPresentationStyle] == UIModalPresentationPageSheet)
        {
        }
        else
        {
             //这里是“核心中的核心”,设置View的Frame,以达到避免遮挡的目的。
             [self setRootViewFrame:rootViewRect];
        }
    }
}

好了,以上是对IQKeyboardManager的使用的介绍以及源码的分析,希望大家对这个非常方便的库有个直观的印象。当然,这里笔者只是做了个简单的介绍,如果大家有兴趣可以深入的看一下它的源代码,相信通过对IQKeyboardManager源代码的阅读,可以加深对iOS中的视图层级的理解。

引用

『零行代码』解决键盘遮挡问题(iOS)
IQKeyboardManager源码分析

你可能感兴趣的:(深入讲解iOS键盘二:使用IQKeyboardManager解决键盘遮挡问题)