Swift防止按鈕重復(fù)點擊實現(xiàn)+Swift如何運用Runtime

Swift防止按鈕重復(fù)點擊實現(xiàn)+Swift如何運用Runtime

做過OC開發(fā)的都知道,我們想要給一個系統(tǒng)的類添加一個屬性我們有幾種方法,比如繼承,我們創(chuàng)建一個父類,給父類寫一個屬性,之后所有使用的類都采用繼承該父類的方式,這樣就會都擁有該屬性.更高級一點的我們會用到OC的Runtime的機制,
給分類添加屬性,即使用 Runtime 中的 objc_setAssociatedObject 和 objc_getAssociatedObject

此外,我們還經(jīng)常使用方法交換Method Swizzling,對一個既有的類進行方法交換,從而完成一些本來不能完成的事情,比如在viewDidAppear:方法調(diào)用的時候想要打印該類,我們通常就會采用方法交換的方式

比如我之前有寫一個防止按鈕重復(fù)點擊的分類
之前是這么寫的

#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;//是否忽略間隔(采用系統(tǒng)默認)

@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機制來構(gòu)造多個屬性,和交換sendAction:to:forEvent: 從而完成在點擊按鈕的時候使其禁用某個間隔時間,以此來防止按鈕重復(fù)點擊造成誤觸

不能說特別好,但起碼是一個比較優(yōu)秀的寫法,這樣我們在創(chuàng)建一個按鈕的時候我們就不用在乎其他,每一個按鈕都會有一個默認的點擊間隔,防止重復(fù)點擊.

我們嘗試采用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 理論上會在加載和初始化類的時候調(diào)用兩個類方法: load and initialize。在講解 method swizzling 的原文中 Mattt 老師指出出于安全性和一致性的考慮,方法交叉過程 永遠 會在 load() 方法中進行。每一個類在加載時只會調(diào)用一次 load 方法。另一方面,一個 initialize 方法可以被一個類和它所有的子類調(diào)用,比如說 UIViewController 的該方法,如果那個類沒有被傳遞信息,那么它的 initialize 方法就永遠不會被調(diào)用了。
不幸的是,在 Swift 中 load 類方法永遠不會被 runtime 調(diào)用,因此方法交叉就變成了不可能的事。我們只能在 initialize 中實現(xiàn)方法交叉,你只需要確保相關(guān)的方法交叉在一個 dispatch_once 中就好了(這也是最推薦的做法)。

不過,Swift使用Method Swizzling需要滿足兩個條件

包含 swizzle 方法的類需要繼承自 NSObject
需要 swizzle 的方法必須有動態(tài)屬性(dynamic attribute)

通常添加下,我們都是交換系統(tǒng)類的方法,一般不需要關(guān)注著兩點。但是如果我們自定義的類,也需要交換方法就要滿足這兩點了( 這種狀況是極少的,也是應(yīng)該避免的 )

表面上來看,感覺并沒有什么,那么我們做個Swift的Runtime的動態(tài)分析
我們拿一個純Swift類和一個繼承自NSObject的類來做分析,這兩個類里包含盡量多的Swift的類型。

首先動態(tài)性比較重要的一點就是能夠拿到某個類所有的方法、屬性

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

我們運行看看結(jié)果


這里寫圖片描述

對于純Swift的SimpleSwiftClass來說任何方法、屬性都未獲取到
對于ViewController來說除testReturnTuple、testReturnCharacter兩個方法外,其他的都獲取成功了。

為什么會發(fā)生這樣的問題呢?

1.純Swift類的函數(shù)調(diào)用已經(jīng)不再是Objective-C的運行時發(fā)消息,而是類似C++的vtable,在編譯時就確定了調(diào)用哪個函數(shù),所以沒法通過Runtime獲取方法、屬性。
2.ViewController繼承自UIViewController,基類為NSObject,而Swift為了兼容Objective-C,凡是繼承自NSObject的類都會保留其動態(tài)性,所以我們能通過Runtime拿到他的方法。
3.從Objective-c的Runtime 特性可以知道,所有運行時方法都依賴TypeEncoding,而Character和Tuple是Swift特有的,無法映射到OC的類型,更無法用OC的typeEncoding表示,也就沒法通過Runtime獲取了。 

再來就是方法交換Method Swizzling

上面已經(jīng)展示了一個防止重復(fù)點擊按鈕的方法交換的例子.
那么就沒有一點問題嗎?

我們就拿現(xiàn)在的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);
    }
}

先寫一個交換方法

然后運行交換方法看看


這里寫圖片描述

運行


這里寫圖片描述

很顯然交換成功,當系統(tǒng)調(diào)用viewDidAppear的時候會交換到myViewDidAppear,
那么我們?nèi)绻\行myViewDidAppear?是不是會交換到viewDidAppear?
這里寫圖片描述

看看運行結(jié)果交換失敗了


這里寫圖片描述

查閱Swift3.0的文檔(之所以不看2.X,不解釋了,與時俱進)

dynamic 將屬性或者方法標記為dynamic就是告訴編譯器把它當作oc里的屬性或方法來使用(runtime),

于是我們將方法和屬性全部加上dynamic修飾


這里寫圖片描述

重新測試


這里寫圖片描述

此時的所有結(jié)果都跟OC的一樣了,原因是什么上面也說了,因為Swift會做靜態(tài)優(yōu)化。要想完全被動態(tài)調(diào)用,必須使用dynamic修飾。
當你指定一個成員變量為 dynamic 時,訪問該變量就總會動態(tài)派發(fā)了。因為在變量定義時指定 dynamic 后,會使用Objective-C Runtime來派發(fā)。

另外再補充一些,
1.Objective-C獲取Swift runtime信息
使用在Objective-c代碼里使用objc_getClass("SimpleSwiftClass");會發(fā)現(xiàn)返回值為空,是因為在OC中的類名已經(jīng)變成SwiftRuntime.SimpleSwiftClass,即規(guī)則為SWIFT_MODULE_NAME.類名稱,在普通源碼項目里SWIFT_MODULE_NAME即為ProductName,在打好的Cocoa Touch Framework里為則為導(dǎo)出的包名。
所以想要獲取信息使用

id cls = objc_getClass("SwiftRuntime.SimpleSwiftClass");
showClassRuntime(cls);

所以O(shè)C替換Swift的方法你應(yīng)該也是會了,確保dynamic,確保類名的正確訪問即可

2.你會發(fā)現(xiàn)initialize方法在Swift3以上會出現(xiàn)警告,表示未來可能會被廢棄
那么我們也可以在 appdelegate 中實現(xiàn)方法交換,不通過類擴展進行方法交換,而是簡單地在 appdelegate 的 application(_:didFinishLaunchingWithOptions:) 方法調(diào)用時中執(zhí)行相關(guān)代碼也是可以的。也可以單獨寫一個用于管理方法交換的單例類,在appdelegate中運行,基于對類的修改,這種方法應(yīng)該就足夠確保這些代碼會被執(zhí)行到。

demo地址:https://github.com/spicyShrimp/SwiftRuntime

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 目錄 Objective-C Runtime到底是什么 Objective-C的元素認知 Runtime詳解 應(yīng)用...
    Ryan___閱讀 1,950評論 1 3
  • 轉(zhuǎn)至元數(shù)據(jù)結(jié)尾創(chuàng)建: 董瀟偉,最新修改于: 十二月 23, 2016 轉(zhuǎn)至元數(shù)據(jù)起始第一章:isa和Class一....
    40c0490e5268閱讀 1,762評論 0 9
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,198評論 4 61
  • 上古無文字,結(jié)繩以記之。如此說來,文字之前,是個浪漫的年代,因為結(jié)繩對于一些事情,保持了神秘的含蓄,不像文字...
    布衣幽香閱讀 1,487評論 0 51
  • 我生病了~停更一次 昨天一直發(fā)低燒,早上打了紅霉素之后,反胃想吐…… 明日更新2篇以上 望請理解哦
    鬼燈森林閱讀 493評論 9 22