RunTime使用案例

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

#pragma mark -- 时间间隔 --
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(UIButton.sendAction(_:to:for:)))
        let swizzMethod = class_getInstanceMethod(UIButton.classForCoder(), #selector(UIButton.mySendAction(_:to:for:)))
        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版代码

#import "UITapGestureRecognizer+JHExtension.h"
#import <objc/runtime.h>

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

@end

static const void *UITapGestureRecognizerduration = @"UITapGestureRecognizerduration";

@implementation UITapGestureRecognizer (JHExtension)

#pragma mark - Getter Setter

- (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版代码

#pragma mark -- 携带多参数 --
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版代码

#import "UIView+JHExtension.h"
#import <objc/runtime.h>
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版代码

#import "UIScrollView+JHRefresh.h"
#import <MJRefresh.h>
@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文件中

#import "UIScrollView+JHEmptyDataSet.h"
#import <objc/runtime.h>

static const void *KClickBlock = @"clickBlock";
static const void *KEmptyText = @"emptyText";
static const void *KOffSet = @"offset";
static const void *Kimage = @"emptyImage";
 
@implementation UIScrollView (JHEmptyDataSet)

#pragma mark - Getter Setter

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

#pragma mark - DZNEmptyDataSetSource

// 空白界面的标题
- (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;
}


#pragma mark - DZNEmptyDataSetDelegate

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

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

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!()
    }
    
}


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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,010评论 4 62
  • 送给年少的我们 文 / 老槐 编辑 / 老槐 图 / 橘子姑娘 君恨我生迟,我恨君生早。 君生我未生,我生君已老...
    老槐走天涯阅读 478评论 0 1
  • 总结: 找到重点 弱化非重点 不断对比 毫不含糊 之前定义的标准可重复使用 色彩不要乱用 分清主次 当不知道用什么...
    youyeath11阅读 348评论 0 2
  • 早餐生意不好没客人时,老板愁;可早餐店生意太好,客人不停催时该怎么办?毕竟一大早大家也赶着上班急着上学。那如何才能...
    老祖宗石磨坊阅读 1,951评论 0 0
  • 1、 已是冬天,眼看就要冬至了,经历了一夏天的闷热,与蚊子的斗智斗勇,在十月下旬的时候,我们把蚊帐给拆掉了,心想着...
    我爱西兰花阅读 293评论 0 0