Runtime学习与使用(1):为UITextField添加类目实现被键盘遮住后视图上移,点击空白回收键盘
Runtime学习与使用(一):为UITextField添加类目实现被键盘遮住后视图上移,点击空白回收键盘
OC中类目无法直接添加属性,可以通过runtime实现在类目中添加属性。
在学习的过程中,试着为UITextField添加了一个类目,实现了当TextField被键盘遮住时视图上移的功能,顺便也添加了点击空白回收键盘功能。
效果预览
使用时不需要一句代码就可以实现上述功能
[github链接](https://github.com/a1419430265/CHTTextFieldHealper)
.h文件
1 // 2 // UITextField+CHTPositionChange.h 3 // CHTTextFieldHealper 4 // 5 // Created by risenb_mac on 16/8/17. 6 // Copyright © 2016年 risenb_mac. All rights reserved. 7 // 8 9 #import <UIKit/UIKit.h> 10 11 @interface UITextField (CHTHealper) 12 13 /** 14 * 是否支持视图上移 15 */ 16 @property (nonatomic, assign) BOOL canMove; 17 /** 18 * 点击回收键盘、移动的视图,默认是当前控制器的view 19 */ 20 @property (nonatomic, strong) UIView *moveView; 21 /** 22 * textfield底部距离键盘顶部的距离 23 */ 24 @property (nonatomic, assign) CGFloat heightToKeyboard; 25 26 @property (nonatomic, assign, readonly) CGFloat keyboardY; 27 @property (nonatomic, assign, readonly) CGFloat keyboardHeight; 28 @property (nonatomic, assign, readonly) CGFloat initialY; 29 @property (nonatomic, assign, readonly) CGFloat totalHeight; 30 @property (nonatomic, strong, readonly) UITapGestureRecognizer *tapGesture; 31 @property (nonatomic, assign, readonly) BOOL hasContentOffset; 32 33 @end
在.h文件中声明属性之后需要在.m中重写setter,getter方法
首先定义全局key用作关联唯一标识符
1 static char canMoveKey; 2 static char moveViewKey;
@implementation UITextField (CHTHealper) @dynamic canMove; @dynamic moveView;
具体实现
1 - (void)setCanMove:(BOOL)canMove { 2 // 参数意义:关联对象 ,关联标识符,关联属性值,关联策略 3 objc_setAssociatedObject(self, &canMoveKey, @(canMove), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 4 } 5 6 - (BOOL)canMove { 7 // 关联属性值为对象类型,需要转换 8 return [objc_getAssociatedObject(self, &canMoveKey) boolValue]; 9 }
想要实现键盘遮住TextField后视图上移,首先应确定TextField是否被键盘遮住,需要知道TextField在整个屏幕中的位置
// 此方法可以获得TextField左上角在当前window中的坐标 [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow]
还需要知道键盘高度,这点需要接受系统通知,但是什么时候接受通知、注销通知?
我的思路是在TextField成为第一响应者的时候,为TextField添加通知,但是如果直接重写becomeFirstResponder方法会覆盖掉UITextField本身的方法,造成的最明显的后果就是没有光标了……为了避免这个问题,我用了runtime另外一个强大的功能,方法交换
为了保证方法交换只进行一次,使用dispatch_once
为了保证方法交换尽早执行,写在了load方法中
1 + (void)load { 2 static dispatch_once_t onceToken; 3 dispatch_once(&onceToken, ^{ 4 SEL systemSel = @selector(initWithFrame:); 5 SEL mySel = @selector(setupInitWithFrame:); 6 [self exchangeSystemSel:systemSel bySel:mySel]; 7 8 SEL systemSel2 = @selector(becomeFirstResponder); 9 SEL mySel2 = @selector(newBecomeFirstResponder); 10 [self exchangeSystemSel:systemSel2 bySel:mySel2]; 11 12 SEL systemSel3 = @selector(resignFirstResponder); 13 SEL mySel3 = @selector(newResignFirstResponder); 14 [self exchangeSystemSel:systemSel3 bySel:mySel3]; 15 16 SEL systemSel4 = @selector(initWithCoder:); 17 SEL mySel4 = @selector(setupInitWithCoder:); 18 [self exchangeSystemSel:systemSel4 bySel:mySel4]; 19 }); 20 [super load]; 21 }
具体交换步骤
1 // 交换方法 2 + (void)exchangeSystemSel:(SEL)systemSel bySel:(SEL)mySel { 3 Method systemMethod = class_getInstanceMethod([self class], systemSel); 4 Method myMethod = class_getInstanceMethod([self class], mySel); 5 //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败 6 BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(myMethod), method_getTypeEncoding(myMethod)); 7 if (isAdd) { 8 //如果成功,说明类中不存在这个方法的实现 9 //将被交换方法的实现替换到这个并不存在的实现 10 class_replaceMethod(self, mySel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod)); 11 }else{ 12 //否则,交换两个方法的实现 13 method_exchangeImplementations(systemMethod, myMethod); 14 } 15 }
在上面我交换了四组方法,两组init方法,是为了保证无论是代码创建的还是xib拖得TextField都进行初始化
1 - (instancetype)setupInitWithCoder:(NSCoder *)aDecoder { 2 [self setup]; 3 return [self setupInitWithCoder:aDecoder]; 4 } 5 6 - (instancetype)setupInitWithFrame:(CGRect)frame { 7 [self setup]; 8 return [self setupInitWithFrame:frame]; 9 } 10 11 - (void)setup { 12 self.heightToKeyboard = 10; 13 self.canMove = YES; 14 self.keyboardY = 0; 15 self.totalHeight = 0; 16 self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)]; 17 }
在TextField成为第一响应者时,为self添加通知接收,为moveView添加点击事件(实现点击空白回收键盘),注销第一响应者时,注销通知,移除点击事件
1 - (BOOL)newBecomeFirstResponder { 2 // 如果没有设置moveView 默认为当前控制器的view 3 if (self.moveView == nil) { 4 self.moveView = [self viewController].view; 5 } 6 // 保证moveView只有一个本TextField的点击事件 7 if (![self.moveView.gestureRecognizers containsObject:self.tapGesture]) { 8 [self.moveView addGestureRecognizer:self.tapGesture]; 9 } 10 // 当重复点击当前TextField时(重复成为第一响应者)或设置为不可移动 不再添加通知 11 if ([self isFirstResponder] || !self.canMove) { 12 return [self newBecomeFirstResponder]; 13 } 14 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showAction:) name:UIKeyboardWillShowNotification object:nil]; 15 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hideAction:) name:UIKeyboardWillHideNotification object:nil]; 16 return [self newBecomeFirstResponder]; 17 } 18 19 - (BOOL)newResignFirstResponder { 20 // 确保当前moveView有当前点击事件,移除 21 if ([self.moveView.gestureRecognizers containsObject:self.tapGesture]) { 22 [self.moveView removeGestureRecognizer:self.tapGesture]; 23 } 24 if (!self.canMove) { 25 return [self newResignFirstResponder]; 26 } 27 BOOL result = [self newResignFirstResponder]; 28 [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; 29 [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; 30 // 当另外一个TextField成为第一响应者,当前TextField注销第一响应者时不会回收键盘,手动调用moveView改变方法 31 [self hideKeyBoard:0]; 32 return result; 33 } 34 //获取当前TextField所在controller 35 - (UIViewController *)viewController { 36 UIView *next = self; 37 while (1) { 38 UIResponder *nextResponder = [next nextResponder]; 39 if ([nextResponder isKindOfClass:[UIViewController class]]) { 40 return (UIViewController *)nextResponder; 41 } 42 next = next.superview; 43 } 44 return nil; 45 }
接收到弹出键盘后调用的方法
1 - (void)showAction:(NSNotification *)sender { 2 if (!self.canMove) { 3 return; 4 } 5 // 获取键盘高度以及键盘的Y坐标 6 self.keyboardY = [sender.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].origin.y; 7 self.keyboardHeight = [sender.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; 8 [self keyboardDidShow]; 9 } 10 11 - (void)hideAction:(NSNotification *)sender { 12 if (!self.canMove || self.keyboardY == 0) { 13 return; 14 } 15 [self hideKeyBoard:0.25]; 16 } 17 18 - (void)keyboardDidShow { 19 if (self.keyboardHeight == 0) { 20 return; 21 } 22 // 获取TextField在window中的Y坐标 23 CGFloat fieldYInWindow = [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow].y; 24 // 确定是否需要视图上移,以及移动的距离 25 CGFloat height = (fieldYInWindow + self.heightToKeyboard + self.frame.size.height) - self.keyboardY; 26 CGFloat moveHeight = height > 0 ? height : 0; 27 28 [UIView animateWithDuration:0.25 animations:^{ 29 // 判断是否是scrollView并进行相应移动 30 if (self.hasContentOffset) { 31 UIScrollView *scrollView = (UIScrollView *)self.moveView; 32 scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y + moveHeight); 33 } else { 34 CGRect rect = self.moveView.frame; 35 self.initialY = rect.origin.y; 36 rect.origin.y -= moveHeight; 37 self.moveView.frame = rect; 38 } 39 // 记录当前TextField使得moveView移动的距离 40 self.totalHeight += moveHeight; 41 }]; 42 } 43 44 - (void)hideKeyBoard:(CGFloat)duration { 45 [UIView animateWithDuration:duration animations:^{ 46 if (self.hasContentOffset) { 47 UIScrollView *scrollView = (UIScrollView *)self.moveView; 48 scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y - self.totalHeight); 49 } else { 50 CGRect rect = self.moveView.frame; 51 rect.origin.y += self.totalHeight; 52 self.moveView.frame = rect; 53 } 54 // moveView回复状态后将移动距离置0 55 self.totalHeight = 0; 56 }]; 57 }
点击事件当前controllerview endediting
- (void)tapAction { [[self viewController].view endEditing:YES]; }