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

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

在学习的过程中,试着为UITextField添加了一个类目,实现了当TextField被键盘遮住时视图上移的功能,顺便也添加了点击空白回收键盘功能。
效果预览
使用时不需要一句代码就可以实现上述功能

gif4.gif

github链接

.h文件

//
//  UITextField+CHTPositionChange.h
//  CHTTextFieldHealper
//
//  Created by risenb_mac on 16/8/17.
//  Copyright © 2016年 risenb_mac. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface UITextField (CHTHealper)

/**
 *  是否支持视图上移
 */
@property (nonatomic, assign) BOOL canMove;
/**
 *  点击回收键盘、移动的视图,默认是当前控制器的view
 */
@property (nonatomic, strong) UIView *moveView;
/**
 *  textfield底部距离键盘顶部的距离
 */
@property (nonatomic, assign) CGFloat heightToKeyboard;

@property (nonatomic, assign, readonly) CGFloat keyboardY;
@property (nonatomic, assign, readonly) CGFloat keyboardHeight;
@property (nonatomic, assign, readonly) CGFloat initialY;
@property (nonatomic, assign, readonly) CGFloat totalHeight;
@property (nonatomic, strong, readonly) UITapGestureRecognizer *tapGesture;
@property (nonatomic, assign, readonly) BOOL hasContentOffset;

@end

在.h文件中声明属性之后需要在.m中重写setter,getter方法
首先定义全局key用作关联唯一标识符

static char canMoveKey;
static char moveViewKey;
@implementation UITextField (CHTHealper)
@dynamic canMove;
@dynamic moveView;

具体实现

- (void)setCanMove:(BOOL)canMove {
// 参数意义:关联对象 ,关联标识符,关联属性值,关联策略
    objc_setAssociatedObject(self, &canMoveKey, @(canMove), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)canMove {
// 关联属性值为对象类型,需要转换
    return [objc_getAssociatedObject(self, &canMoveKey) boolValue];
}

想要实现键盘遮住TextField后视图上移,首先应确定TextField是否被键盘遮住,需要知道TextField在整个屏幕中的位置

// 此方法可以获得TextField左上角在当前window中的坐标
[self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow]

还需要知道键盘高度,这点需要接受系统通知,但是什么时候接受通知、注销通知?
我的思路是在TextField成为第一响应者的时候,为TextField添加通知,但是如果直接重写becomeFirstResponder方法会覆盖掉UITextField本身的方法,造成的最明显的后果就是没有光标了……为了避免这个问题,我用了runtime另外一个强大的功能,方法交换
为了保证方法交换只进行一次,使用dispatch_once
为了保证方法交换尽早执行,写在了load方法中

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL systemSel = @selector(initWithFrame:);
        SEL mySel = @selector(setupInitWithFrame:);
        [self exchangeSystemSel:systemSel bySel:mySel];
        
        SEL systemSel2 = @selector(becomeFirstResponder);
        SEL mySel2 = @selector(newBecomeFirstResponder);
        [self exchangeSystemSel:systemSel2 bySel:mySel2];
        
        SEL systemSel3 = @selector(resignFirstResponder);
        SEL mySel3 = @selector(newResignFirstResponder);
        [self exchangeSystemSel:systemSel3 bySel:mySel3];
        
        SEL systemSel4 = @selector(initWithCoder:);
        SEL mySel4 = @selector(setupInitWithCoder:);
        [self exchangeSystemSel:systemSel4 bySel:mySel4];
    });
    [super load];
}

具体交换步骤

// 交换方法
+ (void)exchangeSystemSel:(SEL)systemSel bySel:(SEL)mySel {
    Method systemMethod = class_getInstanceMethod([self class], systemSel);
    Method myMethod = class_getInstanceMethod([self class], mySel);
    //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败
    BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(myMethod), method_getTypeEncoding(myMethod));
    if (isAdd) {
        //如果成功,说明类中不存在这个方法的实现
        //将被交换方法的实现替换到这个并不存在的实现
        class_replaceMethod(self, mySel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
    }else{
        //否则,交换两个方法的实现
        method_exchangeImplementations(systemMethod, myMethod);
    }
}

在上面我交换了四组方法,两组init方法,是为了保证无论是代码创建的还是xib拖得TextField都进行初始化

- (instancetype)setupInitWithCoder:(NSCoder *)aDecoder {
    [self setup];
    return [self setupInitWithCoder:aDecoder];
}

- (instancetype)setupInitWithFrame:(CGRect)frame {
    [self setup];
    return [self setupInitWithFrame:frame];
}

- (void)setup {
    self.heightToKeyboard = 10;
    self.canMove = YES;
    self.keyboardY = 0;
    self.totalHeight = 0;
    self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)];
}

在TextField成为第一响应者时,为self添加通知接收,为moveView添加点击事件(实现点击空白回收键盘),注销第一响应者时,注销通知,移除点击事件

- (BOOL)newBecomeFirstResponder {
// 如果没有设置moveView 默认为当前控制器的view
    if (self.moveView == nil) {
        self.moveView = [self viewController].view;
    }
// 保证moveView只有一个本TextField的点击事件
    if (![self.moveView.gestureRecognizers containsObject:self.tapGesture]) {
        [self.moveView addGestureRecognizer:self.tapGesture];
    }
// 当重复点击当前TextField时(重复成为第一响应者)或设置为不可移动 不再添加通知
    if ([self isFirstResponder] || !self.canMove) {
        return [self newBecomeFirstResponder];
    }
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showAction:) name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hideAction:) name:UIKeyboardWillHideNotification object:nil];
    return [self newBecomeFirstResponder];
}

- (BOOL)newResignFirstResponder {
// 确保当前moveView有当前点击事件,移除
    if ([self.moveView.gestureRecognizers containsObject:self.tapGesture]) {
        [self.moveView removeGestureRecognizer:self.tapGesture];
    }
    if (!self.canMove) {
        return [self newResignFirstResponder];
    }
    BOOL result = [self newResignFirstResponder];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
// 当另外一个TextField成为第一响应者,当前TextField注销第一响应者时不会回收键盘,手动调用moveView改变方法
    [self hideKeyBoard:0];
    return result;
}
//获取当前TextField所在controller
- (UIViewController *)viewController {
    UIView *next = self;
    while (1) {
        UIResponder *nextResponder = [next nextResponder];
        if ([nextResponder isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)nextResponder;
        }
        next = next.superview;
    }
    return nil;
}

接收到弹出键盘后调用的方法

- (void)showAction:(NSNotification *)sender {
    if (!self.canMove) {
        return;
    }
// 获取键盘高度以及键盘的Y坐标
    self.keyboardY = [sender.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].origin.y;
    self.keyboardHeight = [sender.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height;
    [self keyboardDidShow];
}

- (void)hideAction:(NSNotification *)sender {
    if (!self.canMove || self.keyboardY == 0) {
        return;
    }
    [self hideKeyBoard:0.25];
}

- (void)keyboardDidShow {
    if (self.keyboardHeight == 0) {
        return;
    }
// 获取TextField在window中的Y坐标
    CGFloat fieldYInWindow = [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow].y;
// 确定是否需要视图上移,以及移动的距离
    CGFloat height = (fieldYInWindow + self.heightToKeyboard + self.frame.size.height) - self.keyboardY;
    CGFloat moveHeight = height > 0 ? height : 0;
    
    [UIView animateWithDuration:0.25 animations:^{
// 判断是否是scrollView并进行相应移动
        if (self.hasContentOffset) {
            UIScrollView *scrollView = (UIScrollView *)self.moveView;
            scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y + moveHeight);
        } else {
            CGRect rect = self.moveView.frame;
            self.initialY = rect.origin.y;
            rect.origin.y -= moveHeight;
            self.moveView.frame = rect;
        }
// 记录当前TextField使得moveView移动的距离
        self.totalHeight += moveHeight;
    }];
}

- (void)hideKeyBoard:(CGFloat)duration {
    [UIView animateWithDuration:duration animations:^{
        if (self.hasContentOffset) {
            UIScrollView *scrollView = (UIScrollView *)self.moveView;
            scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y - self.totalHeight);
        } else {
            CGRect rect = self.moveView.frame;
            rect.origin.y += self.totalHeight;
            self.moveView.frame = rect;
        }
// moveView回复状态后将移动距离置0
        self.totalHeight = 0;
    }];
}

点击事件当前controllerview endediting

- (void)tapAction {
    [[self viewController].view endEditing:YES];
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容