一个可以全局使用的键盘监听类-iOS

开发的过程中使用到键盘监听,每个页面都写监听然后再移动页面太繁琐,写了一个类用来全局监听,目前为止基本符合APP开发应用的场景,如果遇到新的问题再更新。代码中使用了ARC,可使用- (void)addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject;替换。

  • 先上使用方法:在APPdelegate的didFinishLaunchingWithOptions中直接子线程设置观察者:
- (void)configKeyboard {
    
    SKeyboardManager *keyboardManager = [[SKeyboardManager alloc] init];
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    //键盘弹出
    [[notificationCenter rac_addObserverForName:UIKeyboardWillChangeFrameNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
        [keyboardManager moveViewsForKeyboardInfo:x];
    }];
   
    [[notificationCenter rac_addObserverForName:UIKeyboardWillHideNotification object:nil] subscribeNext:^(NSNotification * _Nullable x) {
        [keyboardManager moveViewsForKeyboardInfo:x];
    }];
 }
  • 上面代码中的SKeyboardManager是代码核心,我经常性完全不记得自己写的代码是什么意思,所以我备注写的老全面了~~~~~~
    .h
//
//  SKeyboardManager.h
//  Snatch
//
//  Created by DawnWang on 2020/2/9.
//  Copyright © 2020 Dawn Wang. All rights reserved.
//

#import 

NS_ASSUME_NONNULL_BEGIN

@interface SKeyboardManager : NSObject

- (void)moveViewsForKeyboardInfo:(NSNotification *)noti;

@end

NS_ASSUME_NONNULL_END

.m

//
//  SKeyboardManager.m
//  Snatch
//
//  Created by DawnWang on 2020/2/9.
//  Copyright © 2020 Dawn Wang. All rights reserved.
//

#import "SKeyboardManager.h"
#import "NSObject+TopVC.h"
#import "UIView+FirstResponder.h"



/*全局键盘调用的整体思路
1:获取当前VC:topVC
2:获取当前弹出键盘的视图firstResponderView
3:获取需要做滚动的视图moveView
3.1:找到firstResponderView的supperView是否有具有滚动属性的view
3.2:如果有scrollEbleMoveView则直接返回,即moveView = scrollEbleMoveView,如果没有则返回topVC.view,即moveView = topVC.view
3.3:监听scrollEbleMoveView的contentOffset属性,当属性变化的时候,调用[firstResponderView resignFirstResponder](待定,因为resignFirstResponder也可以在外面使用)
4:将firstResponderView的rect映射到moveView上,映射后的结果是convertRect
5:判断convertRect和键盘frame的位置,并让moveView做相应的移动
*/
@interface SKeyboardManager()
<
UIGestureRecognizerDelegate
>

@property (nonatomic, weak) UIViewController *currentTopVC;
@property (nonatomic, strong) UITapGestureRecognizer *tapGesture;
@property (nonatomic, assign) CGFloat moveDistance;

@end
@implementation SKeyboardManager



- (void)moveViewsForKeyboardInfo:(NSNotification *)noti {
    NSDictionary *userInfo = noti.userInfo;
    //1:获取当前VC:topVC
    UIViewController *topVC = [self s_topViewController];
    self.currentTopVC = topVC;
    //2:获取当前弹出键盘的视图firstResponderView
    UIView *firstResponderView = [topVC.view s_firstResponderView];
    //3:获取需要做滚动的视图moveView
    UIView *moveView = [self supperNeedMoveViewFrom:firstResponderView];
    //如果是可滚动视图,给个监听,获取键盘弹出时最后的偏移量,方便键盘消失时恢复原来的样子
    WeakSelf
    if ([moveView isKindOfClass:[UIScrollView class]]) {
        [[moveView rac_valuesForKeyPath:@"contentOffset" observer:nil] subscribeNext:^(id  _Nullable x) {
            if (x) {
                StrongSelf;
                NSValue *value = (NSValue *)x;
                CGPoint point = [value CGPointValue];
                self.moveDistance = point.y;
            }
        }];
    }
    //4:将firstResponderView的rect映射到moveView上,映射后的结果是convertRect
    //此处不映射到moveView上是因为键盘打高度是window坐标,而且当前页面没有隐藏导航,或者scrollview位置偏下会造成计算错误
    CGRect convertRect = [firstResponderView convertRect:firstResponderView.bounds toView:[UIApplication sharedApplication].keyWindow];
  
    //5:判断convertRect和键盘frame的位置,并让moveView做相应的移动
    CGRect originalFrame = moveView.frame;
    if ([noti.name isEqualToString:UIKeyboardWillChangeFrameNotification]) {
        CGRect keyboardFameEnd = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
        CGFloat movedDistance = convertRect.origin.y + convertRect.size.height - keyboardFameEnd.origin.y;
        //键盘frame有变化且需要移动
        if (movedDistance > 0) {
            if ([moveView isMemberOfClass:[UIScrollView class]]) {
                //如果是可移动视图,直接移动
                UIScrollView *scrollView = (UIScrollView *)moveView;
                [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y + movedDistance) animated:YES];
             }else{
                 //如果是vc.view则重新设置origin.y
                 [UIView animateWithDuration:[userInfo[UIKeyboardAnimationDurationUserInfoKey] integerValue] animations:^{
                     moveView.frame = CGRectMake(CGRectGetMinX(originalFrame), originalFrame.origin.y - movedDistance, CGRectGetWidth(originalFrame), CGRectGetHeight(originalFrame));
                 } completion:^(BOOL finished) {
                 }];
             }
        }
    }

    if ([noti.name isEqualToString:UIKeyboardWillHideNotification]) {
             
        if ([moveView isMemberOfClass:[UIScrollView class]]) {
            UIScrollView *scrollView = (UIScrollView *)moveView;
            [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y - self.moveDistance) animated:YES];
        }else{
            
            [UIView animateWithDuration:[userInfo[UIKeyboardAnimationDurationUserInfoKey] integerValue] animations:^{
                moveView.frame = CGRectMake(CGRectGetMinX(originalFrame), 0, CGRectGetWidth(moveView.frame), CGRectGetHeight(moveView.frame));
            } completion:^(BOOL finished) {
            }];
            
        }

    }
 
    
    //topvc tap 收起键盘
    if (![topVC.view.gestureRecognizers containsObject:self.tapGesture]) {
        UITapGestureRecognizer *tapGes = [[UITapGestureRecognizer alloc] init];
        [topVC.view addGestureRecognizer:tapGes];
        tapGes.delegate = self;
        self.tapGesture = tapGes;
    }
    __weak typeof(firstResponderView) weakFirstView = firstResponderView;
    [[self.tapGesture rac_gestureSignal] subscribeNext:^(__kindof UIGestureRecognizer * _Nullable x) {
        __strong typeof(weakFirstView) strongFirstView = weakFirstView;
        [strongFirstView resignFirstResponder];
    }];
}

//3.1:找到firstResponderView的supperView是否有具有滚动属性的view
//3.2:如果有scrollEbleMoveView则直接返回,即moveView = scrollEbleMoveView,如果没有则返回topVC.view,即moveView = topVC.view
- (UIView *)supperNeedMoveViewFrom:(UIView *)view {
    //TODO::辨别可滚动视图
    UIView *tempSupperView = view;
    while (![NSStringFromClass(tempSupperView.classForCoder) isEqualToString:@"UIViewControllerWrapperView"]) {
        if ([tempSupperView isMemberOfClass:[UIScrollView class]]) {
            return tempSupperView;
        }else{
            tempSupperView = tempSupperView.superview;
        }
    }
        
    return self.currentTopVC.view;
}
//此处修复了一个bug是,UICollectionViewCell的点击操作跟我的键盘收回的手势冲突了
#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    UIView *touchView = touch.view;
    BOOL findCell = YES;
    while (![touchView.nextResponder isKindOfClass:[UICollectionViewCell class]]) {
        if ([NSStringFromClass(touchView.classForCoder) isEqualToString:@"UIViewControllerWrapperView"]) {
            findCell = NO;
            break;
        }
        touchView = (UIView *)touchView.nextResponder;
    }
    return !findCell;
}
@end

  • 辅助代码为查找当前试图VC和第一响应者
//
//  NSObject+TopVC.m
//  Snatch
//
//  Created by DawnWang on 2020/2/9.
//  Copyright © 2020 Dawn Wang. All rights reserved.
//

#import "NSObject+TopVC.h"


@implementation NSObject (TopVC)
- (UIViewController *)s_topViewController {
    UIViewController *currentVC = nil;
    UIWindow *window = [UIApplication sharedApplication].keyWindow;
    if (window) {
        currentVC = window.rootViewController;
    }
    return [self scanController:currentVC];
}

- (UIViewController *)scanController:(UIViewController *)viewController {
    if (!viewController) {
        return nil;
    }
    UIViewController *currentVC = nil;
    if ([viewController isKindOfClass:[UINavigationController class]] && ([((UINavigationController *)viewController) topViewController] != nil)) {
        //当前根试图是navigationVCm,且topVC不为空
        currentVC = [self scanController:[(UINavigationController *)viewController topViewController]];
    }else if ([viewController isKindOfClass:[UITabBarController class]] && ([((UITabBarController *)viewController)  selectedViewController] != nil)){
        //当前根试图是tabbar vc,且selectedVC不为空
        currentVC = [self scanController:[(UITabBarController *)viewController selectedViewController]];
    }else{
        //当前试图是一个标准UIViewController
        currentVC = viewController;
        //判断是否是present出来的试图
        BOOL isPresentController = NO;
        UIViewController *presentVC = currentVC.presentedViewController;
        while (presentVC) {
            currentVC = presentVC;
            isPresentController = YES;
            presentVC = currentVC.presentedViewController;
        }
        if (isPresentController) {
            currentVC = [self scanController:currentVC];
        }
    }
    return currentVC;
}

@end

//
//  UIView+FirstResponder.m
//  Snatch
//
//  Created by DawnWang on 2020/2/9.
//  Copyright © 2020 Dawn Wang. All rights reserved.
//

#import "UIView+FirstResponder.h"

@implementation UIView (FirstResponder)
- (UIView * __nullable)s_firstResponderView {
    if ([self isFirstResponder]) {
        return self;
    }
    UIView *firstView = nil;
    for (UIView *subView in self.subviews) {
        firstView = [subView s_firstResponderView];
        if ([firstView isFirstResponder]) {
            break;
        }
    }
    return firstView;
}

@end

好了,代码罗列完毕。

  • 开发过程中全局控制键盘出现一个超级大bug:XPC connection interrupted,导致页面完全卡死不能动了,原因是在键盘弹出的情况下直接返回上一级页面的时候,当前VC先释放,然后再发送键盘收起的通知,导致[self s_topViewController]拿不到正确的VC,解决方法是在根navigationController里面重新定义了pop方法:
- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
    //fix bug : 当前VC中尚有textfield调起键盘时后返回,导致的:XPC connection interrupted错误
    UIViewController * topVC = [self topViewController];
    UIView *firstResponse = [topVC.view s_firstResponderView];
    [firstResponse resignFirstResponder];
    return [super popViewControllerAnimated:animated];
}

最后附上一个小知识点:键盘通知顺序

  • 调起键盘通知顺序:UIKeyboardWillChangeFrameNotification->UIKeyboardWillShowNotification->UIKeyboardDidChangeFrameNotification->UIKeyboardDidShowNotification
  • 点击小地球切换键盘模式的时候,调用顺序同上,但是只有当键盘frame有变化时才调用;
  • 在两个UITextField中切换的时候,通知顺序同上,此时不调用Hide的通知。
  • 键盘隐藏时调用顺序:UIKeyboardWillChangeFrameNotification->UIKeyboardWillHideNotification->UITextFieldTextDidEndEditingNotification->UIKeyboardDidChangeFrameNotification->UIKeyboardDidHideNotification
  • 题外话:当textfield使用中文输入的时候,点击键盘和输入汉字时都会发送UITextFieldTextDidChangeNotification通知。

3.12更新
问题:VC上添加UITextField,如果先设置UITextField.text = @"abc";再调起键盘重新修改内容,会报一个错误:[Snapshotting] Snapshotting a view (0x7f93eb22f660, UIInputSetContainerView) that has not been rendered at least once requires afterScreenUpdates:YES.。此时的UITextField没有正常释放,只有再次进入页面并让textField becomeFirstResponser,才会释放。查了资料说是系统bug,暂无修改方法。我的键盘管理类,此时获取到的topVC为空,因为使用了MLeaksFinder。所以在方法- (void)moveViewsForKeyboardInfo:(NSNotification *)noti 中添加一个判断就可以,if (!topVC) { return; }

你可能感兴趣的:(一个可以全局使用的键盘监听类-iOS)