RunTime的使用案例

RunTime的使用案例
From: https://juejin.im/post/5ade99faf265da0b886d0c49

RunTime这个概念几乎是老生常谈了,但是有一些人对这个一直是仅仅对概念的理解,对于用到实例的次数并不太多,这里我就来说一下我项目中一些用到的实例方法吧,里面包含OC和Swift双版本。要是对RunTime的基础该要还有一些不了解的同学,可以点击这里,进行一些概念的普及。

案例

  • 1、防止Button的暴力点击
  • 2、防止UITapGestureRecognizer的暴力点击
  • 3、扩大button的点击范围
  • 4、UIButton 点击事件带多参数
  • 5、给View添加ViewID标志
  • 6、全局返回手势
  • 7、对MJRefresh的封装
  • 8、对DZNEmptyDataSet的封装

1、防止Button的暴力点击

第一篇案例,就说一篇网络上到处都有的一类文章吧,网上一搜,满满的都是。大家应该对这个也是特别的了解吧,所以先从这里开始。

感觉OC版的都没有什么难点,需要注意的Swift版的交换时机。

OC版代码

+ (void)load{
    Method originalMethod = class_getInstanceMethod([self class], @selector(sendAction:to:forEvent:));
    Method swizzledMethod = class_getInstanceMethod([self class], @selector(JH_SendAction:to:forEvent:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}


static const void *ButtonDurationTime = @"ButtonDurationTime";
- (NSTimeInterval)durationTime{
    NSNumber *number = objc_getAssociatedObject(self, &ButtonDurationTime);
    return number.doubleValue;
}
- (void)setDurationTime:(NSTimeInterval)durationTime{
    NSNumber *number = [NSNumber numberWithDouble:durationTime];
    objc_setAssociatedObject(self, &ButtonDurationTime, number, OBJC_ASSOCIATION_COPY_NONATOMIC);

}

- (void)JH_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{

    self.userInteractionEnabled = NO;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.durationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.userInteractionEnabled = YES;
    });

    [self JH_SendAction:action to:target forEvent:event];
}
复制代码

这里有三个小知识点

  • 1、+ (void)load{} 它是一个在整个文件被加载到运行时,在 main 函数调用之前被 ObjC 运行时调用的方法

规则一:父类先于子类调用 规则二:类先于分类调用

  • 2、objc_setAssociatedObject & objc_getAssociatedObject 给分类添加属性
  • 3、method_exchangeImplementations替换原方法实现

Swift版代码 我们知道在swift中取消了+load方法,然后swift4.0以后initialize()也被禁用了,所以想要在哪了实现交换方法还真的需要考虑一下了。这里我想到了三种方法
//部分代码是转载后觉得缺失代码新增的步骤, 函数名没更换,但是逻辑步骤是对的

  • 1、交换方法实用OC代码,然后用一个桥连接
  • 2、把交换方法放到application(_ application:, didFinishLaunchingWithOptions launchOptions: )中,这样交换方法也只会调用一次
  • 3、写一个静态方法,这里我就是实用的静态方法

    struct RunTimeButtonKey {

    ///连续两次点击相差时间
    static let timeInterval = UnsafeRawPointer.init(bitPattern: "timeInterval".hashValue)
    

    }
    extension UIButton {

    private static let changeFunction: () = {
        //交换方法
        let systemMethod = class_getInstanceMethod(UIButton.classForCoder(), #selector(self.sendAction))
        let swizzMethod = class_getInstanceMethod(UIButton.classForCoder(), #selector(self.swizzeMethod))
     //class_replaceMethod(object_getClass(self), #selector(self.swizzeMethod), method_getImplementation(originaMethodC), method_getTypeEncoding(originaMethodC))
       method_exchangeImplementations(systemMethod!, swizzMethod!)
    
        print("changeFunction")
    
    }()
    
    //添加属性,在设置 timeInterval 的时候 修改button的执行事件
    var timeInterval: CGFloat? {
        set {
            objc_setAssociatedObject(self, RunTimeButtonKey.timeInterval!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
    
            UIButton.changeFunction
        }
    
        get {
    
            return  objc_getAssociatedObject(self, RunTimeButtonKey.timeInterval!) as? CGFloat
        }
    
    }
    
    @objc private dynamic func mySendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
        self.isUserInteractionEnabled = false
        let time:TimeInterval = TimeInterval(timeInterval ?? 0.0)
        DispatchQueue.main.asyncAfter(deadline:.now() + time) {
            self.isUserInteractionEnabled = true
        }
        mySendAction(action, to: target, for: event)
    }
    

    }
    复制代码

其实我对这个方法也是不太满意,但是现在也没有想到更好的方法,哪位小伙伴想到了更好的方法,可以跟我交流一下

2、防止UITapGestureRecognizer的暴力点击

这里为什么要把UITapGestureRecognizer暴力点击也单独拿出来讨论一下呢,因为前一段时间项目中有很多的是执行的点击事件,但是因为是多处用到了,所以就想添加一个timeInterval来处理,但是我当时走到了一个误区。

当时我是参考防止Button的暴力点击的思路的,当时我在UITapGestureRecognizer中找到了addTarget:(id)target action:(SEL)action 这个方法,然后我就想着用一个自己写的方法来交换这个方法,因为button里面有sendAction:to:forEvent:,我当时不动脑子,直接就认为他们一样了,其实他们是有很大区别的sendAction:to:forEvent: 这个是执行的方法,我们交换的话就是交换的是执行方法,但是addTarget:(id)target action:(SEL)action 是添加方法,即使我们交换了,在执行的时候并没有什么变化的

后来一个偶然想起了可以在代理里面改变执行,思路就是添加一个timeInterval,然后在代理里面根据timeInterval设置UITapGestureRecognizer是否可用

OC版代码

@interface UITapGestureRecognizer ()
///时间间隔
@property (nonatomic,assign) NSTimeInterval duration;

@end

static const void *UITapGestureRecognizerduration = @"UITapGestureRecognizerduration";

@implementation UITapGestureRecognizer (JHExtension)



- (NSTimeInterval)duration{
    NSNumber *number = objc_getAssociatedObject(self, &UITapGestureRecognizerduration);
    return number.doubleValue;
}

- (void)setDuration:(NSTimeInterval)duration{
    NSNumber *number = [NSNumber numberWithDouble:duration];
    objc_setAssociatedObject(self, &UITapGestureRecognizerduration, number, OBJC_ASSOCIATION_COPY_NONATOMIC);
}



/**
 添加点击事件

 @param target taeget
 @param action action
 @param duration 时间间隔
 */
- (instancetype)initWithTarget:(id)target action:(SEL)action withDuration:(NSTimeInterval)duration{

    self = [super init];
    if (self) {
        self.duration = duration;
        self.delegate = self;
        [self addTarget:target action:action];
    }
    return self;

}


- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
    self.enabled = NO;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.enabled = YES;
    });

    return YES;
}

@end
复制代码

我们使用UITapGestureRecognizer的时候,我们这么使用[[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(事件)];,所以我直接把方法定义为initWithTarget:(id)target action:(SEL)action withDuration:(NSTimeInterval)duration这样就不改变我们平时的书写习惯了。

Swift版代码

import UIKit
struct RunTimeTapGestureKey {
    ///连续两次点击相差时间
    static let timeInterval = UnsafeRawPointer.init(bitPattern: "timeInterval".hashValue)
}

extension UITapGestureRecognizer:UIGestureRecognizerDelegate{
    //添加属性,在设置 timeInterval 的时候
    var timeInterval: CGFloat? {
        set {
            objc_setAssociatedObject(self, RunTimeTapGestureKey.timeInterval!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
            self.delegate = self
        }
        get {
            return  objc_getAssociatedObject(self, RunTimeTapGestureKey.timeInterval!) as? CGFloat
        }
    }


    convenience init(target: Any?, action: Selector?,timeInterval:CGFloat) {
        self.init(target: target, action: action)
        self.timeInterval = timeInterval
        self.delegate = self  
    }


    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        self.isEnabled = false
        let time:TimeInterval = TimeInterval(timeInterval ?? 0.0)
        DispatchQueue.main.asyncAfter(deadline:.now() + time) {
            self.isEnabled = true
        }
        return true
    }
}

复制代码

3、扩大button的点击范围

开始之前我们首先需要介绍一个方法- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

iOS系统检测到手指触摸(Touch)操作时会将其放入当前活动Application的事件队列,UIApplication会从事件队列中取出触摸事件并传递给key window(当前接收用户事件的窗口)处理,window对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,称之为hit-test view。

window对象会在首先在view hierarchy的顶级view上调用hitTest:withEvent:,此方法会在视图层级结构中的每个视图上调用pointInside:withEvent:,如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是hit-test view。

hitTest:withEvent:方法的处理流程如下:

  1. 首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
  2. 若返回NO,则hitTest:withEvent:返回nil;
  3. 若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
  4. 若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
  5. 如所有子视图都返回非,则hitTest:withEvent:方法返回自身(self)。

操作思路

  • 1、我们自己添加属性,重新设置点击区域大小
  • 2、根据新的点击区域,重写hitTest:withEvent:方法

OC版代码

static const void *topNameKey = @"topNameKey";
static const void *rightNameKey = @"rightNameKey";
static const void *bottomNameKey = @"bottomNameKey";
static const void *leftNameKey = @"leftNameKey";


- (void)setEnlargeEdgeWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left{

    objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
    objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (CGRect)enlargedRect
{
    NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);
    NSNumber *rightEdge = objc_getAssociatedObject(self, &rightNameKey);
    NSNumber *bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
    NSNumber *leftEdge = objc_getAssociatedObject(self, &leftNameKey);
    if (topEdge && rightEdge && bottomEdge && leftEdge) {
        return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
                          self.bounds.origin.y - topEdge.floatValue,
                          self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
                          self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
    }
    else
    {
        return self.bounds;
    }
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    CGRect rect = [self enlargedRect];
    if (CGRectEqualToRect(rect, self.bounds)) {
        return [super hitTest:point withEvent:event];
    }
    return CGRectContainsPoint(rect, point) ? self : nil;
}

复制代码

Swift版代码

//MARK: -- 扩大点击响应事件 --
struct RunTimeButtonKey {

    ///点击区域
    static let topNameKey = UnsafeRawPointer.init(bitPattern: "topNameKey".hashValue)
    static let rightNameKey = UnsafeRawPointer.init(bitPattern: "rightNameKey".hashValue)
    static let bottomNameKey = UnsafeRawPointer.init(bitPattern: "bottomNameKey".hashValue)
    static let leftNameKey = UnsafeRawPointer.init(bitPattern: "leftNameKey".hashValue)

}
extension UIButton {

    var topEdge: CGFloat? {
        set {
            objc_setAssociatedObject(self, RunTimeButtonKey.topNameKey!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
            UIButton.changeFunction
        }

        get {

            return  objc_getAssociatedObject(self, RunTimeButtonKey.topNameKey!) as? CGFloat
        }
    }
     var leftEdge: CGFloat? {
        set {
            objc_setAssociatedObject(self, RunTimeButtonKey.leftNameKey!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
            UIButton.changeFunction
        }

        get {

            return  objc_getAssociatedObject(self, RunTimeButtonKey.leftNameKey!) as? CGFloat
        }
    }
     var rightEdge: CGFloat? {
        set {
            objc_setAssociatedObject(self, RunTimeButtonKey.rightNameKey!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
            UIButton.changeFunction
        }

        get {

            return  objc_getAssociatedObject(self, RunTimeButtonKey.rightNameKey!) as? CGFloat
        }
    }

    var bottomEdge: CGFloat? {
        set {
            objc_setAssociatedObject(self, RunTimeButtonKey.bottomNameKey!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
            UIButton.changeFunction
        }

        get {

            return  objc_getAssociatedObject(self, RunTimeButtonKey.bottomNameKey!) as? CGFloat
        }
    }


    /// 扩大点击区域
    ///
    /// - Parameters:
    ///   - top: 上
    ///   - right: 右
    ///   - bottom: 下
    ///   - left: 左
    func setEnlargeEdge(top:CGFloat,right:CGFloat,bottom:CGFloat,left:CGFloat)  {
        self.topEdge = top
        self.rightEdge = right
        self.bottomEdge = bottom
        self.leftEdge = left

    }

    open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let left = self.leftEdge ?? 0
        let right = self.rightEdge ?? 0
        let bottom = self.bottomEdge ?? 0
        let top = self.topEdge ?? 0

        let rect:CGRect = CGRect(x: self.bounds.origin.x - left,
                             y: self.bounds.origin.y - top,
                             width: self.bounds.size.width + left + right, height: self.bounds.size.height + top + bottom)


        return rect.contains(point) ? self : nil
    }

}

复制代码

4、UIButton 点击事件带多参数

iOS 原生的 UIButton 点击事件是不允许带多参数的,唯一的一个参数就是默认UIButton本身 那么我们该怎么实现传递多个参数的点击事件呢?

  • 1、如果业务场景非常简单,要求传单参数并且是整数类型,可以用tag
  • 2、利用ObjC关联,runtime之所以被称为iOS 的动态特性是有道理的,当然关联甚至可以帮助NSArray等其他对象实现“多参数传递”

OC版代码

static const void *RunTimeButtonParam = @"RunTimeButtonParam";
- (NSDictionary*)ButtonParam{
    NSDictionary *param = objc_getAssociatedObject(self, &RunTimeButtonParam);
    return param;
}
- (void)setButtonParam:(NSDictionary *)ButtonParam{
    objc_setAssociatedObject(self, &RunTimeButtonParam, ButtonParam, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

复制代码

Swift版代码

//MARK: -- 携带参数 --
extension UIButton {
    var buttonParam: Dictionary<String, Any>? {
        set {
            objc_setAssociatedObject(self, RunTimeButtonKey.RunTimeButtonParam!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }

        get {
            return  objc_getAssociatedObject(self, RunTimeButtonKey.RunTimeButtonParam!) as? Dictionary
        }
    }
}
复制代码

5、给View添加ViewID标志

其实这个跟第四个案例是一样的,但是这里写出来是为了让大家有一个跟多的对比,同时也可以扩展一下思维。

为什么我会给view添加ViewID标志呢,在前一段时间做项目的时候,我需要给view添加标志,标记我点了哪一个view,我们知道我们iOS开发中tag是int类型的,如果后台给我们的id都是值类型的那一般都没有什么太大问题,关键是有的时候后台的id是字符串类型,有字母也有数字,这个时候我们就不能用tag来标记了,而使用字符串类型的ViewID标志那就十分的适合了。

OC版代码

static const void *RunTimeViewID = @"RunTimeViewID";
static const void *RunTimeViewParam = @"RunTimeViewParam";

@implementation UIView (JHExtension)

- (NSString *)viewID{
    NSString *ID = objc_getAssociatedObject(self, &RunTimeViewID);
    return ID;
}
- (void)setViewID:(NSString *)viewID{
    objc_setAssociatedObject(self, &RunTimeViewID, viewID, OBJC_ASSOCIATION_COPY_NONATOMIC);
}



- (NSDictionary *)viewParam{
    NSDictionary *param = objc_getAssociatedObject(self, &RunTimeViewParam);
    return param;
}
- (void)setViewParam:(NSDictionary *)viewParam{
     objc_setAssociatedObject(self, &RunTimeViewParam, viewParam, OBJC_ASSOCIATION_COPY_NONATOMIC);
}


@end

复制代码

Swift版代码

struct RunTimeViewKey {
    static let RunTimeViewID = UnsafeRawPointer.init(bitPattern: "RunTimeViewID".hashValue)
    static let RunTimeViewParam = UnsafeRawPointer.init(bitPattern: "RunTimeViewParam".hashValue)
}

extension UIView {
    var ViewID: String? {
        set {
            objc_setAssociatedObject(self, RunTimeViewKey.RunTimeViewID!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)

        }
        get {
            return  objc_getAssociatedObject(self, RunTimeViewKey.RunTimeViewID!) as? String
        }
    }

    var ViewParam: Dictionary<String, Any>? {
        set {
            objc_setAssociatedObject(self, RunTimeViewKey.RunTimeViewParam!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)

        }
        get {
            return  objc_getAssociatedObject(self, RunTimeViewKey.RunTimeViewParam!) as? Dictionary
        }
    }
}
复制代码

6、全局返回手势

对于全局返回手势,虽然能够知道原理,但是因为跟原生手势有交互,本着对自己技术不相信的态度,我就简单的说一下原理,具体的我们还是最好使用大神的。

其实系统是自带返回手势的,但是他的返回手势是在最左边,我们要做的就是找到这个这个系统方法,然后把他对象设置给控制器的View

 // 打印系统自带滑动手势的代理对象
    NSLog(@"%@",self.interactivePopGestureRecognizer.delegate);

复制代码

我们发现打印方法为handleNavigationTransition

然后我们就可以上代码了

OC版代码

- (void)viewDidLoad {
    [super viewDidLoad];

    // 获取系统自带滑动手势的target对象
    id target = self.interactivePopGestureRecognizer.delegate;

    // 创建全屏滑动手势,调用系统自带滑动手势的target的action方法
    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:target action:@selector(handleNavigationTransition:)];

    // 设置手势代理,拦截手势触发
    pan.delegate = self;

    // 给导航控制器的view添加全屏滑动手势
    [self.view addGestureRecognizer:pan];

    // 禁止使用系统自带的滑动手势
    self.interactivePopGestureRecognizer.enabled = NO;

}

// 什么时候调用:每次触发手势之前都会询问下代理,是否触发。
// 作用:拦截手势触发
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
    // 注意:只有非根控制器才有滑动返回功能,根控制器没有。
    // 判断导航控制器是否只有一个子控制器,如果只有一个子控制器,肯定是根控制器
    if (self.childViewControllers.count == 1) {
        // 表示用户在根控制器界面,就不需要触发滑动手势,
        return NO;
    }
    return YES;
}

复制代码

注意:这些方法是写在UINavigationController里面的

文章参考自:【8行代码教你搞定导航控制器全屏滑动返回效果】 |那些人追的干货

Swift版代码

  override func viewDidLoad() {
        super.viewDidLoad()
        let target = self.interactivePopGestureRecognizer?.delegate
        let pan = UIPanGestureRecognizer(target: target, action: Selector(("handleNavigationTransition:")))
        pan.delegate = self
        self.view.addGestureRecognizer(pan)
        // 禁止使用系统自带的滑动手势
        self.interactivePopGestureRecognizer?.isEnabled = false;
    }

    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if (self.childViewControllers.count == 1) {
            // 表示用户在根控制器界面,就不需要触发滑动手势,
            return false;
        }
        return true;
    }
复制代码

这两个demo没有用到runtime的方法,但是也是用到了替代方法, 是一个点赞量接近5千的demo,里面运用了运行时, 这里 有一篇介绍的文章

7、对MJRefresh的封装

想必大部分人都用过MJRefresh这个刷新控件吧,在我刚开始使用的时候,在每一个刷新的地方都会重新的定义一下这个控件,每一个tableview中都会创建这个刷新控件,然后把他的属性在写一遍,但是作为一个程序员怎么容忍自己写那么多的无意义代码呢。我们知道tableviewcollectionView都是继承自scrollView,那么我们可以在 scrollView的分类里面添加一些方法,那么我们在以后使用的时候,就不需要一遍一遍的重复写无用代码了,只需要调用scrollView分类方法就可以了。

OC版代码

@implementation UIScrollView (JHRefresh)
/**
 添加刷新事件

 @param headerBlock 头部刷新
 @param footerBlock 底部刷新
 */
- (void)setRefreshWithHeaderBlock:(void(^)(void))headerBlock
                      footerBlock:(void(^)(void))footerBlock{
    if (headerBlock) {

        MJRefreshNormalHeader *header= [MJRefreshNormalHeader headerWithRefreshingBlock:^{
            if (headerBlock) {
                headerBlock();
            }
        }];
        header.stateLabel.font = [UIFont systemFontOfSize:13];
        header.lastUpdatedTimeLabel.font = [UIFont systemFontOfSize:13];

        self.mj_header = header;
    }

    if (footerBlock) {
        MJRefreshBackNormalFooter *footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
            footerBlock();
        }];
        footer.stateLabel.font = [UIFont systemFontOfSize:13];
        [footer setTitle:@"暂无更多数据" forState:MJRefreshStateNoMoreData];
        [footer setTitle:@"" forState:MJRefreshStateIdle];
        self.mj_footer.ignoredScrollViewContentInsetBottom = 44;
        self.mj_footer = footer;
    }
}



/**
 开启头部刷新
 */
- (void)headerBeginRefreshing{
    [self.mj_header beginRefreshing];
}


/**
 没有更多数据
 */
- (void)footerNoMoreData{
    [self.mj_footer setState:MJRefreshStateNoMoreData];
}

/**
 结束刷新
 */
- (void)endRefresh{

    if (self.mj_header) {
        [self.mj_header endRefreshing];
    }
    if (self.mj_footer) {
        [self.mj_footer endRefreshing];
    }
}


复制代码

Swift版代码

import UIKit
import MJRefresh
extension UIScrollView {

    /// 添加刷新事件
    ///
    /// - Parameters:
    ///   - refreshHeaderClosure: 头部刷新
    ///   - refreshFooterClosure: 底部刷新
    func addRefreshWithScrollView(refreshHeaderClosure:@escaping()->(), refreshFooterClosure:@escaping()->())  {

        ///*******头部刷新*************
        let header:MJRefreshNormalHeader = MJRefreshNormalHeader.init {
           refreshHeaderClosure()
        }

        //自动改变透明度 (当控件被导航条挡住后不显示)
        header.isAutomaticallyChangeAlpha = true
        // 设置字体
        header.stateLabel.font = UIFont.systemFont(ofSize: 13)
        header.lastUpdatedTimeLabel.font = UIFont.systemFont(ofSize: 13)
        self.mj_header = header

        ///**********尾部刷新**********
        let foot:MJRefreshBackNormalFooter = MJRefreshBackNormalFooter.init {
            refreshFooterClosure()
        }

        foot.stateLabel.font = UIFont.systemFont(ofSize: 13)
        foot.setTitle("", for: MJRefreshState.idle)
        foot.setTitle("暂无更多数据", for: MJRefreshState.noMoreData)

        self.mj_footer = foot

    }


    /// 添加头部刷新事件
    ///
    /// - Parameter refreshClosure: 闭包回调
    func addRefreshHeaderWithScrollView(refreshClosure:@escaping()->()) {

        let header:MJRefreshNormalHeader = MJRefreshNormalHeader.init {
            refreshClosure()
            }  

        //自动改变透明度 (当控件被导航条挡住后不显示)
        header.isAutomaticallyChangeAlpha = true
        // 设置字体
        header.stateLabel.font = UIFont.systemFont(ofSize: 13)
        header.lastUpdatedTimeLabel.font = UIFont.systemFont(ofSize: 13)
        self.mj_header = header
    }

    /// 下拉加载
    ///
    /// - Parameters:
    ///   -  tableView: tableView
    ///   - refreshClosure: 闭包回调
    func addRefreshFooterWithScrollView(refreshClosure:@escaping()->()) {
        let foot:MJRefreshBackNormalFooter = MJRefreshBackNormalFooter.init {
            refreshClosure()
        }

        foot.stateLabel.font = UIFont.systemFont(ofSize: 13)
        foot.setTitle("", for: MJRefreshState.idle)
        foot.setTitle("暂无更多数据", for: MJRefreshState.noMoreData)

        self.mj_footer = foot

    }


    /// 结束刷新
    ///
    /// - Parameter tableView: tableView
    func endRefreshWithTableView() {

        if (self.mj_header != nil) {
            self.mj_header.endRefreshing()
        }
        if (self.mj_footer != nil) {
            self.mj_footer.endRefreshing()
        }

    }


    /// 没有数据
    func NOMoreData() {
        self.mj_footer.state = .noMoreData
    }

}

复制代码

8、对DZNEmptyDataSet的封装

其实这个跟上面那一个封装是一个类型的,oc版代码几乎都是一样的,但是swift代码里面会有一个小小的坑需要我们来填。

OC版代码.h文件里面暴露了一下的方法

@property (nonatomic) ClickBlock clickBlock;                // 点击事件
@property (nonatomic, assign) CGFloat offset;               // 垂直偏移量
@property (nonatomic, strong) NSString *emptyText;          // 空数据显示内容
@property (nonatomic, strong) UIImage *emptyImage;          // 空数据的图片


- (void)setupEmptyData:(ClickBlock)clickBlock;
- (void)setupEmptyDataText:(NSString *)text tapBlock:(ClickBlock)clickBlock;
- (void)setupEmptyDataText:(NSString *)text verticalOffset:(CGFloat)offset tapBlock:(ClickBlock)clickBlock;
- (void)setupEmptyDataText:(NSString *)text verticalOffset:(CGFloat)offset emptyImage:(UIImage *)image tapBlock:(ClickBlock)clickBlock;
复制代码

.m文件中

static const void *KClickBlock = @"clickBlock";
static const void *KEmptyText = @"emptyText";
static const void *KOffSet = @"offset";
static const void *Kimage = @"emptyImage";

@implementation UIScrollView (JHEmptyDataSet)



- (ClickBlock)clickBlock{
    return objc_getAssociatedObject(self, &KClickBlock);
}

- (void)setClickBlock:(ClickBlock)clickBlock{

    objc_setAssociatedObject(self, &KClickBlock, clickBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (NSString *)emptyText{
    return objc_getAssociatedObject(self, &KEmptyText);
}

- (void)setEmptyText:(NSString *)emptyText{
    objc_setAssociatedObject(self, &KEmptyText, emptyText, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (CGFloat)offset{

    NSNumber *number = objc_getAssociatedObject(self, &KOffSet);
    return number.floatValue;
}

- (void)setOffset:(CGFloat)offset{

    NSNumber *number = [NSNumber numberWithDouble:offset];

    objc_setAssociatedObject(self, &KOffSet, number, OBJC_ASSOCIATION_COPY_NONATOMIC);
}


- (UIImage *)emptyImage{
    return objc_getAssociatedObject(self, &Kimage);
}

- (void)setEmptyImage:(UIImage *)emptyImage{
    objc_setAssociatedObject(self, &Kimage, emptyImage, OBJC_ASSOCIATION_COPY_NONATOMIC);
}



- (void)setupEmptyData:(ClickBlock)clickBlock{
    self.clickBlock = clickBlock;
    self.emptyDataSetSource = self;
    if (clickBlock) {
        self.emptyDataSetDelegate = self;
    }
}


- (void)setupEmptyDataText:(NSString *)text tapBlock:(ClickBlock)clickBlock{

    self.clickBlock = clickBlock;
    self.emptyText = text;

    self.emptyDataSetSource = self;
    if (clickBlock) {
        self.emptyDataSetDelegate = self;
    }
}


- (void)setupEmptyDataText:(NSString *)text verticalOffset:(CGFloat)offset tapBlock:(ClickBlock)clickBlock{

    self.emptyText = text;
    self.offset = offset;
    self.clickBlock = clickBlock;

    self.emptyDataSetSource = self;
    if (clickBlock) {
        self.emptyDataSetDelegate = self;
    }
}


- (void)setupEmptyDataText:(NSString *)text verticalOffset:(CGFloat)offset emptyImage:(UIImage *)image tapBlock:(ClickBlock)clickBlock{

    self.emptyText = text;
    self.offset = offset;
    self.emptyImage = image;
    self.clickBlock = clickBlock;

    self.emptyDataSetSource = self;
    self.emptyDataSetDelegate = self;

}



// 空白界面的标题
- (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView{
    NSString *text = self.emptyText?:@"没有找到任何数据";
    UIFont *font = [UIFont systemFontOfSize:17.0];
    NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:text];
    [attStr addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, text.length)];
    [attStr addAttribute:NSForegroundColorAttributeName value:JHWordColorDark range:NSMakeRange(0, text.length)];

    return attStr;
}

// 空白页的图片
- (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView{
    return self.emptyImage?:[UIImage imageNamed:@"mine"];
}

//是否允许滚动,默认NO
- (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView {
    return YES;
}

// 垂直偏移量
- (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView{
    return self.offset;
}




- (void)emptyDataSet:(UIScrollView *)scrollView didTapView:(UIView *)view{
    if (self.clickBlock) {
        self.clickBlock();
    }
}

复制代码

swift版代码 这里出现了一个坑,就是swift中怎么在扩展中添加闭包回调属性。对于这个问题大家可以参考[这篇文章][5],关键就是一句话,要先定义一个类属性作为闭包容器,专门存放闭包的属性

[5]: https://link.juejin.im?target=https%3A%2F%2Fwww.jianshu.com%2Fp%2Fc6658ee16168

import UIKit
import DZNEmptyDataSet

struct RuntimeKey {
    ///空数据显示内容
    static let emptyText = UnsafeRawPointer.init(bitPattern: "emptyText".hashValue)
    ///空数据的图片
    static let emptyImage = UnsafeRawPointer.init(bitPattern: "emptyImage".hashValue)
    ///垂直偏移量
    static let offset = UnsafeRawPointer.init(bitPattern: "offset".hashValue)
    ///点击回调闭包
    static var clickClosure = UnsafeRawPointer.init(bitPattern: "clickClosure".hashValue)

}
//MARK: -- 给UIScrollView添加属性 --
extension UIScrollView {
    ///空数据显示内容
    var emptyText: String? {
        set {
            objc_setAssociatedObject(self, RuntimeKey.emptyText!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }

        get {
            return  objc_getAssociatedObject(self, RuntimeKey.emptyText!) as? String
        }
    }
    ///空数据的图片
    var emptyImage: String? {
        set {
            objc_setAssociatedObject(self, RuntimeKey.emptyImage!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }

        get {
            return  objc_getAssociatedObject(self, RuntimeKey.emptyImage!) as? String
        }
    }

    ///垂直偏移量
    var offset: CGFloat? {
        set {
            objc_setAssociatedObject(self, RuntimeKey.offset!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }

        get {
            return  objc_getAssociatedObject(self, RuntimeKey.offset!) as? CGFloat
        }
    }

    //闭包回调
    typealias clickTipClosure = () -> Void
    // 定义一个类属性作为闭包的容器,专门存放闭包的属性
    private class BlockContainer: NSObject, NSCopying {
        func copy(with zone: NSZone? = nil) -> Any {
            return self
        }
        var clickTipClosure: clickTipClosure?
    }
    // 定义个一个计算属性
    private var newDataBlock: BlockContainer? {

        set(newValue) {
            objc_setAssociatedObject(self, RuntimeKey.clickClosure!, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
        }
        get {
            return objc_getAssociatedObject(self, RuntimeKey.clickClosure!) as? BlockContainer
        }

    }


}

//MARK: -- 给UIScrollView添加方法 --
extension UIScrollView :DZNEmptyDataSetSource,DZNEmptyDataSetDelegate{

    /// 设置空白页text。image。偏移量
    ///
    /// - Parameters:
    ///   - text: text
    ///   - image: image
    ///   - offSet: 偏移量
    func SetUPEmptyTextWithEmptyImageWithOffSet(text:String,image:String,offSet:CGFloat) {
        self.emptyText = text
        self.emptyImage = image
        self.offset = offSet

        self.emptyDataSetDelegate = self
        self.emptyDataSetSource = self
    }

    /// 设置空白页text。image
    ///
    /// - Parameters:
    ///   - text: text
    ///   - image: image
    ///
    func SetUPEmptyTextWithEmptyImage(text:String,image:String){
        self.emptyText = text
        self.emptyImage = image


        self.emptyDataSetDelegate = self
        self.emptyDataSetSource = self
    }


    /// 仅仅设置空白页图片
    ///
    /// - image: image
    func SetUPEmptyText(image:String){
         self.emptyImage = image
         self.emptyDataSetDelegate = self
         self.emptyDataSetSource = self
    }


    /// 仅仅设置空白页文本
    ///
    /// - Parameter text: text
    func SetUPEmptyText(text:String){
        self.emptyText = text
        self.emptyDataSetDelegate = self
        self.emptyDataSetSource = self
    }

    ///点击空白页回调
    func obtainClickClosure(Closure:@escaping clickTipClosure) {
        // 创建blockContainer,将外界传来的闭包赋值给类属性中的闭包变量
        let blockContainer: BlockContainer = BlockContainer()
        blockContainer.clickTipClosure = Closure
        self.newDataBlock = blockContainer
    }


}




//MARK: - DZNEmptyDataSetSource -
extension UIScrollView {
    // 空白界面的标题
    public func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! {

        guard self.emptyText != nil else {
            return nil
        }

        let text = self.emptyText ?? ""
        let attStr = NSMutableAttributedString.init(string: text)
        attStr.addAttribute(NSAttributedStringKey.strokeColor, value: UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 1), range: NSMakeRange(0, text.count))

        attStr.addAttribute(NSAttributedStringKey.font, value: UIFont.systemFont(ofSize: 17), range: NSMakeRange(0, text.count))

        return attStr
    }
    // 空白页的图片
    public func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! {

        guard self.emptyImage != nil else {
            return nil
        }

        return UIImage.init(named: self.emptyImage!)
    }

    //是否允许滚动,默认NO
    public func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool {
        return true
    }

    // 垂直偏移量
    public func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat {
        let set = self.offset ?? -50.0
        return CGFloat(set)
    }

    //点击
    public func emptyDataSet(_ scrollView: UIScrollView!, didTap view: UIView!) {
        self.newDataBlock?.clickTipClosure!()
    }

}


复制代码