一个可以全局使用的键盘监听类-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通知。
问题: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;
}
推荐阅读
- 一个人的旅行,三亚
- 一个小故事,我的思考。
- 一个人的碎碎念
- 七年之痒之后
- 我从来不做坏事
- 异地恋中,逐渐适应一个人到底意味着什么()
- 迷失的世界(二十七)
- live|live to inspire 一个普通上班族的流水账0723
- 遗憾是生活的常态,但孝顺这件事,我希望每一个人都不留遗憾
- NO.38|NO.38 我不是嫁不出去,而是不想嫁