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 @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:方法的处理流程如下:
首先调用当前视图的pointInside:withEvent:方法判断触摸点是否在当前视图内;
若返回NO,则hitTest:withEvent:返回nil;
若返回YES,则向当前视图的所有子视图(subviews)发送hitTest:withEvent:消息,所有子视图的遍历顺序是从top到bottom,即从subviews数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
若第一次有子视图返回非空对象,则hitTest:withEvent:方法返回此对象,处理结束;
如所有子视图都返回非,则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? { 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 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? { 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; } 00 func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if (self.childViewControllers.count == 1) { // 表示用户在根控制器界面,就不需要触发滑动手势, return false; } return true; }
这两个demo没有用到runtime的方法,但是也是用到了替代方法,
这是一个点赞量接近5千的demo,里面运用了运行时,
这里有一篇介绍的文章
7、对MJRefresh的封装
想必大部分人都用过MJRefresh这个刷新控件吧,在我刚开始使用的时候,在每一个刷新的地方都会重新的定义一下这个控件,每一个tableview中都会创建这个刷新控件,然后把他的属性在写一遍,但是作为一个程序员怎么容忍自己写那么多的无意义代码呢。我们知道tableview和collectionView都是继承自scrollView,那么我们可以在 scrollView的分类里面添加一些方法,那么我们在以后使用的时候,就不需要一遍一遍的重复写无用代码了,只需要调用scrollView分类方法就可以了。
OC版代码
#import "UIScrollView+JHRefresh.h" #import @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 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!() } }
作者:辉哥de简书
链接:https://www.jianshu.com/p/a00f99ecf276