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的封装
第一篇案例,就说一篇网络上到处都有的一类文章吧,网上一搜,满满的都是。大家应该对这个也是特别的了解吧,所以先从这里开始。
感觉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
}
}
复制代码
开始之前我们首先需要介绍一个方法- (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
}
}
复制代码
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中都会创建这个刷新控件,然后把他的属性在写一遍,但是作为一个程序员怎么容忍自己写那么多的无意义代码呢。我们知道tableview
和collectionView
都是继承自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!()
}
}
复制代码