Runtime学习与使用(1):为UITextField添加类目实现被键盘遮住后视图上移,点击空白回收键盘

Runtime学习与使用(一):为UITextField添加类目实现被键盘遮住后视图上移,点击空白回收键盘

OC中类目无法直接添加属性,可以通过runtime实现在类目中添加属性。

在学习的过程中,试着为UITextField添加了一个类目,实现了当TextField被键盘遮住时视图上移的功能,顺便也添加了点击空白回收键盘功能。
效果预览
使用时不需要一句代码就可以实现上述功能
Runtime学习与使用(1):为UITextField添加类目实现被键盘遮住后视图上移,点击空白回收键盘
[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];
}