Swift防止按钮重复点击实现+Swift如何运用Runtime

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 UIKit

class 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

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

推荐阅读更多精彩内容

  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 1,923评论 1 3
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,945评论 4 60
  • 上古无文字,结绳以记之。如此说来,文字之前,是个浪漫的年代,因为结绳对于一些事情,保持了神秘的含蓄,不像文字...
    布衣幽香阅读 1,436评论 0 51
  • 我生病了~停更一次 昨天一直发低烧,早上打了红霉素之后,反胃想吐…… 明日更新2篇以上 望请理解哦
    鬼灯森林阅读 478评论 9 22