一个可以全局使用的键盘监听类-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; @endNS_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 = https://www.it610.com/article/(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通知。
【一个可以全局使用的键盘监听类-iOS】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; }

    推荐阅读