你的位置:首页 > 操作系统

[操作系统]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  * 点击回收键盘、移动的视图,默认是当前控制器的view19 */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所在controller35 - (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回复状态后将移动距离置055     self.totalHeight = 0;56   }];57 }

 

点击事件当前controllerview endediting

- (void)tapAction {  [[self viewController].view endEditing:YES];}