美文网首页iOS功能模块收录首页投稿(暂停使用,暂停投稿)程序员
Runtime学习与使用(一):为UITextField添加类目

Runtime学习与使用(一):为UITextField添加类目

作者: 大傻子的小想法 | 来源:发表于2016-08-19 13:42 被阅读366次

    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];
    }
    

    相关文章

      网友评论

        本文标题:Runtime学习与使用(一):为UITextField添加类目

        本文链接:https://www.haomeiwen.com/subject/agebsttx.html