Swift防止按钮重复点击实现+Swift如何运用Runtime
来源:互联网 发布:mac spss24破解码 编辑:程序博客网 时间:2024/06/06 10:49
Swift防止按钮重复点击实现+Swift如何运用Runtime
做过OC开发的都知道,我们想要给一个系统的类添加一个属性我们有几种方法,比如继承,我们创建一个父类,给父类写一个属性,之后所有使用的类都采用继承该父类的方式,这样就会都拥有该属性.更高级一点的我们会用到OC的Runtime的机制,
给分类添加属性,即使用 Runtime 中的 objc_setAssociatedObject 和 objc_getAssociatedObject
此外,我们还经常使用方法交换Method Swizzling,对一个既有的类进行方法交换,从而完成一些本来不能完成的事情,比如在viewDidAppear:方法调用的时候想要打印该类,我们通常就会采用方法交换的方式
比如我之前有写一个防止按钮重复点击的分类
之前是这么写的
#import <UIKit/UIKit.h>#ifndef xlx_defaultInterval#define xlx_defaultInterval 0.5 //默认时间间隔#endif@interface UIButton (Interval)@property (nonatomic, assign) NSTimeInterval customInterval;//自定义时间间隔@property (nonatomic, assign) BOOL ignoreInterval;//是否忽略间隔(采用系统默认)@end#import "UIButton+Interval.h"#import <objc/runtime.h>@implementation UIButton (Interval)static const char *xlx_customInterval = "xlx_customInterval";- (NSTimeInterval)customInterval { return [objc_getAssociatedObject(self, xlx_customInterval) doubleValue];}- (void)setCustomInterval:(NSTimeInterval)customInterval { objc_setAssociatedObject(self, xlx_customInterval, @(customInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}static const char *xlx_ignoreInterval = "xlx_ignoreInterval";-(BOOL)ignoreInterval { return [objc_getAssociatedObject(self, xlx_ignoreInterval) integerValue];}-(void)setIgnoreInterval:(BOOL)ignoreInterval { objc_setAssociatedObject(self, xlx_ignoreInterval, @(ignoreInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);}+ (void)load{ if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL systemSel = @selector(sendAction:to:forEvent:); SEL swizzSel = @selector(mySendAction:to:forEvent:); Method systemMethod = class_getInstanceMethod([self class], systemSel); Method swizzMethod = class_getInstanceMethod([self class], swizzSel); BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod)); if (isAdd) { class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod)); }else{ method_exchangeImplementations(systemMethod, swizzMethod); } }); }}- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{ if (!self.ignoreInterval) { [self setUserInteractionEnabled:NO]; [self performSelector:@selector(resumeUserInteractionEnabled) withObject:nil afterDelay:self.customInterval>0?self.customInterval:xlx_defaultInterval]; } [self mySendAction:action to:target forEvent:event];}-(void)resumeUserInteractionEnabled{ [self setUserInteractionEnabled:YES];}@end
这里有采用OC的Runtime机制来构造多个属性,和交换sendAction:to:forEvent: 从而完成在点击按钮的时候使其禁用某个间隔时间,以此来防止按钮重复点击造成误触
不能说特别好,但起码是一个比较优秀的写法,这样我们在创建一个按钮的时候我们就不用在乎其他,每一个按钮都会有一个默认的点击间隔,防止重复点击.
我们尝试采用Swift的方式来写看看
extension DispatchQueue { static var `default`: DispatchQueue { return DispatchQueue.global(qos: .`default`) } static var userInteractive: DispatchQueue { return DispatchQueue.global(qos: .userInteractive) } static var userInitiated: DispatchQueue { return DispatchQueue.global(qos: .userInitiated) } static var utility: DispatchQueue { return DispatchQueue.global(qos: .utility) } static var background: DispatchQueue { return DispatchQueue.global(qos: .background) } func after(_ delay: TimeInterval, execute closure: @escaping () -> Void) { asyncAfter(deadline: .now() + delay, execute: closure) } private static var _onceTracker = [String]() public class func once(_ token: String, block:()->Void) { objc_sync_enter(self) defer { objc_sync_exit(self) } if _onceTracker.contains(token) { return } _onceTracker.append(token) block() }}extension UIButton { private struct AssociatedKeys { static var xlx_defaultInterval:TimeInterval = 0.5 static var xlx_customInterval = "xlx_customInterval" static var xlx_ignoreInterval = "xlx_ignoreInterval" } var customInterval: TimeInterval { get { let xlx_customInterval = objc_getAssociatedObject(self, &AssociatedKeys.xlx_customInterval) if let time = xlx_customInterval { return time as! TimeInterval }else{ return AssociatedKeys.xlx_defaultInterval } } set { objc_setAssociatedObject(self, &AssociatedKeys.xlx_customInterval, newValue as TimeInterval ,.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } var ignoreInterval: Bool { get { return (objc_getAssociatedObject(self, &AssociatedKeys.xlx_ignoreInterval) != nil) } set { objc_setAssociatedObject(self, &AssociatedKeys.xlx_ignoreInterval, newValue as Bool, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } override open class func initialize() { if self == UIButton.self { DispatchQueue.once(NSUUID().uuidString, block: { let systemSel = #selector(UIButton.sendAction(_:to:for:)) let swizzSel = #selector(UIButton.mySendAction(_:to:for:)) let systemMethod = class_getInstanceMethod(self, systemSel) let swizzMethod = class_getInstanceMethod(self, swizzSel) let isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod)) if isAdd { class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod)); }else { method_exchangeImplementations(systemMethod, swizzMethod); } }) } } private dynamic func mySendAction(_ action: Selector, to target: Any?, for event: UIEvent?) { if !ignoreInterval { isUserInteractionEnabled = false DispatchQueue.main.after(customInterval) { [weak self] in self?.isUserInteractionEnabled = true } } mySendAction(action, to: target, for: event) }}
一切都是并不是那么麻烦
但是要注意的是Objective-C runtime 理论上会在加载和初始化类的时候调用两个类方法: load and initialize。在讲解 method swizzling 的原文中 Mattt 老师指出出于安全性和一致性的考虑,方法交叉过程 永远 会在 load() 方法中进行。每一个类在加载时只会调用一次 load 方法。另一方面,一个 initialize 方法可以被一个类和它所有的子类调用,比如说 UIViewController 的该方法,如果那个类没有被传递信息,那么它的 initialize 方法就永远不会被调用了。
不幸的是,在 Swift 中 load 类方法永远不会被 runtime 调用,因此方法交叉就变成了不可能的事。我们只能在 initialize 中实现方法交叉,你只需要确保相关的方法交叉在一个 dispatch_once 中就好了(这也是最推荐的做法)。
不过,Swift使用Method Swizzling需要满足两个条件
包含 swizzle 方法的类需要继承自 NSObject需要 swizzle 的方法必须有动态属性(dynamic attribute)
通常添加下,我们都是交换系统类的方法,一般不需要关注着两点。但是如果我们自定义的类,也需要交换方法就要满足这两点了( 这种状况是极少的,也是应该避免的 )
表面上来看,感觉并没有什么,那么我们做个Swift的Runtime的动态分析
我们拿一个纯Swift类和一个继承自NSObject的类来做分析,这两个类里包含尽量多的Swift的类型。
首先动态性比较重要的一点就是能够拿到某个类所有的方法、属性
import UIKitclass SimpleSwiftClass { var aBool:Bool = true var aInt:Int = 10 var aDouble:Double = 3.1415926 var aString:String = "SimpleSwiftClass" var aObj:AnyObject! = nil func testNoReturn() { }}class ViewController: UIViewController { var aBool:Bool = true var aInt:Int = 10 var aDouble:Double = 3.1415926 var aString:String = "SimpleSwiftClass" var aObj:AnyObject! = nil override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view, typically from a nib. let aSwiftClass:SimpleSwiftClass = SimpleSwiftClass(); showClassRuntime(object_getClass(aSwiftClass)); print("\n"); showClassRuntime(object_getClass(self)); } func testNoReturn() { } func testReturnBool() -> Bool { return true; } func testReturnInt() -> Int { return 10; } func testReturnDouble() -> Double { return 10.1111; } func testReturnString() -> String { return "a boy"; } //OC中没有的TypeEncoding func testReturnTuple() -> (Bool,String) { return (true,"aaa"); } func testReturnCharacter() -> Character { return "a"; }}func showClassRuntime(_ aclass:AnyClass) { print(aclass) print("start methodList") var methodNum:UInt32 = 0 let methodList = class_copyMethodList(aclass, &methodNum) for index in 0..<numericCast(methodNum) { let method:Method = methodList![index]! print(method_getName(method)) } print("end methodList") print("start propertyList") var propertyNum:UInt32 = 0 let propertyList = class_copyPropertyList(aclass, &propertyNum) for index in 0..<numericCast(propertyNum) { let property:objc_property_t = propertyList![index]! print(String(utf8String: property_getName(property)) ?? "") } print("end propertyList")}
我们运行看看结果
对于纯Swift的SimpleSwiftClass来说任何方法、属性都未获取到
对于ViewController来说除testReturnTuple、testReturnCharacter两个方法外,其他的都获取成功了。
为什么会发生这样的问题呢?
1.纯Swift类的函数调用已经不再是Objective-C的运行时发消息,而是类似C++的vtable,在编译时就确定了调用哪个函数,所以没法通过Runtime获取方法、属性。2.ViewController继承自UIViewController,基类为NSObject,而Swift为了兼容Objective-C,凡是继承自NSObject的类都会保留其动态性,所以我们能通过Runtime拿到他的方法。3.从Objective-c的Runtime 特性可以知道,所有运行时方法都依赖TypeEncoding,而Character和Tuple是Swift特有的,无法映射到OC的类型,更无法用OC的typeEncoding表示,也就没法通过Runtime获取了。
再来就是方法交换Method Swizzling
上面已经展示了一个防止重复点击按钮的方法交换的例子.
那么就没有一点问题吗?
我们就拿现在的ViewController来举个例子
func methodSwizze(cls:AnyClass, systemSel:Selector,swizzeSel:Selector) { let systemMethod = class_getInstanceMethod(cls, systemSel) let swizzeMethod = class_getInstanceMethod(cls, swizzeSel) let isAdd = class_addMethod(cls, systemSel, method_getImplementation(swizzeMethod), method_getTypeEncoding(swizzeMethod)) if isAdd { class_replaceMethod(cls, swizzeSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod)); }else { method_exchangeImplementations(systemMethod, swizzeMethod); }}
先写一个交换方法
然后运行交换方法看看
运行
很显然交换成功,当系统调用viewDidAppear的时候会交换到myViewDidAppear,
那么我们如果运行myViewDidAppear?是不是会交换到viewDidAppear?
看看运行结果交换失败了
查阅Swift3.0的文档(之所以不看2.X,不解释了,与时俱进)
dynamic 将属性或者方法标记为dynamic就是告诉编译器把它当作oc里的属性或方法来使用(runtime),
于是我们将方法和属性全部加上dynamic修饰
重新测试
此时的所有结果都跟OC的一样了,原因是什么上面也说了,因为Swift会做静态优化。要想完全被动态调用,必须使用dynamic修饰。
当你指定一个成员变量为 dynamic 时,访问该变量就总会动态派发了。因为在变量定义时指定 dynamic 后,会使用Objective-C Runtime来派发。
另外再补充一些,
1.Objective-C获取Swift runtime信息
使用在Objective-c代码里使用objc_getClass(“SimpleSwiftClass”);会发现返回值为空,是因为在OC中的类名已经变成SwiftRuntime.SimpleSwiftClass,即规则为SWIFT_MODULE_NAME.类名称,在普通源码项目里SWIFT_MODULE_NAME即为ProductName,在打好的Cocoa Touch Framework里为则为导出的包名。
所以想要获取信息使用
id cls = objc_getClass("SwiftRuntime.SimpleSwiftClass");showClassRuntime(cls);
所以OC替换Swift的方法你应该也是会了,确保dynamic,确保类名的正确访问即可
2.你会发现initialize方法在Swift3以上会出现警告,表示未来可能会被废弃
那么我们也可以在 appdelegate 中实现方法交换,不通过类扩展进行方法交换,而是简单地在 appdelegate 的 application(_:didFinishLaunchingWithOptions:) 方法调用时中执行相关代码也是可以的。也可以单独写一个用于管理方法交换的单例类,在appdelegate中运行,基于对类的修改,这种方法应该就足够确保这些代码会被执行到。
demo地址:https://github.com/spicyShrimp/SwiftRuntime
我刚刚开始有在写iOS开发的系列,欢迎访问
系列:iOS开发-前言+大纲
http://blog.csdn.net/spicyShrimp/article/details/62218521
- Swift防止按钮重复点击实现+Swift如何运用Runtime
- 几个常用的Swift Extension:防止按钮重复点击、增加友盟页面统计、Dispatch_once Swift实现
- 防止按钮重复点击的js实现
- 防止按钮重复点击
- Swift 防止快速连续点击"提交"按钮,造成多次提交
- 利用RunTime Hook 实现iOS防止按钮连续响应点击
- 简单防止按钮重复点击
- Android 防止重复按钮点击
- jquery防止按钮重复点击
- asp.net中如何防止用户重复点击提交按钮
- 【asp.net中如何防止用户重复点击提交按钮】
- Swift-UIButton规避重复点击
- swift selector 按钮点击事件
- swift-tableView 代理方法实现点击按钮删除效果
- ios:点击tabbar中间按钮弹出菜单(swift 3实现)
- android 防止 多次点击 重复点击按钮
- android 按钮防止连续点击防止按钮重复点击
- Swift runtime
- hihoCoder
- Quake4 在地图中放置GUI
- BZOJ 3329: Xorequ 数位DP+矩阵快速幂
- 山东省第八届ACM省赛 G 题(sum of power)
- equals与==的区别
- Swift防止按钮重复点击实现+Swift如何运用Runtime
- 晓说——东瀛日本(一)
- POJ NOI0105-44 第n小的质数
- CentOS7使用firewalld打开关闭防火墙与端口
- 关于全排列
- 转换日期格式
- golang实现自定义驱动的Cache
- linux下安装安装jdk和安装android studio
- 搜索--07