使用純Swift編寫(xiě)一個(gè)事件代理

在日常開(kāi)發(fā)中我們經(jīng)常會(huì)遇到這樣的場(chǎng)景,有很多模塊的delegate需要通過(guò)一個(gè)公共類來(lái)轉(zhuǎn)發(fā)回調(diào)事件戳护。比如采用MVP模式開(kāi)發(fā)一個(gè)復(fù)雜的UI交互,其中(許多)View要通過(guò)Presenter來(lái)轉(zhuǎn)發(fā)網(wǎng)絡(luò)回調(diào)、文件訪問(wèn)、數(shù)據(jù)庫(kù)等多種不同的Delegate的回調(diào)事件睬辐。標(biāo)準(zhǔn)做法是在Presenter中實(shí)現(xiàn)各個(gè)不同的Delegate,然后再轉(zhuǎn)發(fā)給(多個(gè))View宾肺。這樣做繁瑣且代碼重復(fù)溯饵,更重要的是不方便對(duì)多個(gè)View做回調(diào)控制。例如:用戶在首次觀看視頻時(shí)锨用,點(diǎn)擊「退出」按鈕會(huì)響應(yīng)「視頻退出事件」此時(shí)會(huì)彈出一個(gè)confirm丰刊,如果選擇「取消」則中斷后續(xù)的事件回調(diào)。對(duì)于這樣的場(chǎng)景使用proxy是最方便的增拥。如果有這么一個(gè)Proxy啄巧,則Presenter不需要轉(zhuǎn)發(fā)一次事件回調(diào)洪橘,而只要將(多個(gè))view添加到proxy接收Delegate的回調(diào)就可以了。如果用ObjeC來(lái)實(shí)現(xiàn)proxy是非常簡(jiǎn)單的棵帽,只需要實(shí)現(xiàn)三個(gè)runtime方法即可:

/** 判斷是否可以響應(yīng)Selector */
- (BOOL)respondsToSelector:(SEL)aSelector {
    for (id target in self.targets) {
        if ([target respondsToSelector:aSelector]) {
            return YES;
        }
    }
    return NO;
}

/** 解析方法簽名 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    NSMethodSignature *signature = nil;
    for (id target in self.targets) {
        if ((signature = [target methodSignatureForSelector:selector])) {
            break;
        }
    }
    return signature;
}

/** 消息轉(zhuǎn)發(fā) */
- (void)forwardInvocation:(NSInvocation *)invocation {
    for (id target in targets) {
        // 這里可添加更多的「控制」
        [context invokeWithTarget:target];
    }
}

但是對(duì)于pure swift來(lái)說(shuō),因?yàn)閯?dòng)態(tài)能力幾乎為零——使用Mirror只能做些簡(jiǎn)單的屬性操作而不能對(duì)方法做反射操作渣玲,所以要實(shí)現(xiàn)類似上面ObjC版的proxy功能需要解決三個(gè)問(wèn)題:

  1. pure swift中的protocol沒(méi)有@optional逗概,也即沒(méi)有respondsToSelector的能力
  2. proxy是一個(gè)通用的實(shí)現(xiàn),而delegate中的方法簽名是各不相同的忘衍,也即沒(méi)有@selector的能力
  3. swift是泛型+類型推導(dǎo)的編程范式逾苫,而各回調(diào)方法的參數(shù)類型是完全不同的便脊。另外proxy內(nèi)部維護(hù)target列表的類型必然是Any——也就是無(wú)類型呵扛。這就導(dǎo)致無(wú)法通過(guò)「as」強(qiáng)轉(zhuǎn)出原方法簽名并執(zhí)行,也即沒(méi)有invocation等能力

因?yàn)樯鲜龅娜齻€(gè)理由陷寝,在pure swift的實(shí)現(xiàn)版本中代碼冗余一些是必然的但至少能實(shí)現(xiàn)搀捷。下面我們來(lái)逐個(gè)解決上面列出的問(wèn)題:

  1. 對(duì)于第一個(gè)問(wèn)題星掰,所有的靜態(tài)語(yǔ)言都只能用繼承(多態(tài))來(lái)解決。還好swift的extension(或者其它支持類mixin的靜態(tài)語(yǔ)言)可以借助編譯器做到多態(tài)而又不會(huì)侵入到target中(所有單繼承的OO語(yǔ)言嫩舟,對(duì)于繼承是即愛(ài)又恨)
  2. 第二個(gè)問(wèn)題的標(biāo)準(zhǔn)做法還是繼承氢烘,通過(guò)一個(gè)proxy的子類實(shí)現(xiàn)所有會(huì)用到的delegate的協(xié)議。在swift中則是通過(guò)extension來(lái)包裝proxy的「通用」invoke方法家厌。這個(gè)方式可以很方便的解決方法簽名的強(qiáng)類型校驗(yàn)播玖,但缺點(diǎn)是delegate中的每一個(gè)方法都需要包裝一遍,其實(shí)這只能算是強(qiáng)類型語(yǔ)言的特點(diǎn)不能算缺陷饭于。另外還可以用swift 5.0中的@dynamicCallable + @dynamicmemberlookup實(shí)現(xiàn)對(duì)delegate中方法的訪問(wèn)「動(dòng)態(tài)」訪問(wèn)蜀踏,只不過(guò)實(shí)現(xiàn)起來(lái)還會(huì)更繁瑣一些
  3. 第三個(gè)問(wèn)題與第二個(gè)問(wèn)題是緊密相連的,如果采用@dynamicCallable + @dynamicmemberlookup方式則可以使用([String : Any?]) -> (Instance) -> ()對(duì)delegate的各方法做currying掰吕。如果采用包裝proxy的「通用」invoke方法的實(shí)現(xiàn)方式則可以通過(guò)(T) -> ()對(duì)個(gè)方法做currying果覆,其中T為delegate協(xié)議類型。

結(jié)合下面的代碼會(huì)更容易理解上面的解釋:

/**
 *  通過(guò)事件代理
    TODO:
    1. 并發(fā)控制
    2. 設(shè)定target響應(yīng)優(yōu)先級(jí)
    3. 對(duì)target設(shè)定所在的回調(diào)線程
 */
class Proxy {
    /** 緩存轉(zhuǎn)發(fā)的target列表 */
    private var targets: [Any] = []
    /** 記錄事件流掛起/終止情況 */
    private var suspendOfEvents: [String: (Bool, Bool)] = [:]
    /** 記錄恢復(fù)事件流時(shí)對(duì)應(yīng)target的Index和需要執(zhí)行的回調(diào)closure */
    private var resumeOfEvents: [String: (Int,  (Any) -> () -> ())] = [:]
    
    /** 添加目標(biāo)對(duì)象 */
    func addTarget(target: Any) {
        targets.append(target)
    }
    
    /** 掛起事件流 */
    @discardableResult
    func suspend(signature: String = #function,
                 isInterrupt: Bool = false) -> ((Bool) -> ())? {
        if suspendOfEvents.keys.contains(signature) {
            // 標(biāo)記掛起
            suspendOfEvents[signature] = (true, isInterrupt)
            // 將resume包裝成(isInterrupt) -> ()殖熟,便于外部使用
            return {
                [weak self] isInterrupt in
                if let self = self {
                    self.resume(signature: signature, isInterrupt: isInterrupt)
                }
            }
        } else {
            return nil
        }
    }
    
    /** 恢復(fù)事件流 */
    func resume(signature: String, isInterrupt: Bool = false) {
        if let (idx, handler) = resumeOfEvents[signature] {
            // 標(biāo)記恢復(fù)
            suspendOfEvents[signature] = (false, isInterrupt)
            // 繼續(xù)執(zhí)行后續(xù)的事件流
            invoking(signature: signature, index: idx, handler: handler)
        }
    }
    
    /** 通用的事件回調(diào)執(zhí)行器 */
    func invoke<T>(signature: String = #function,
                   apply: @escaping (T) -> ()) {
        // 刪除未執(zhí)行完的事件流
        removeSuspendEvent(signature: signature)
        // 記錄執(zhí)行情況
        suspendOfEvents[signature] = (false, false)
        // 執(zhí)行事件流随静,將外部傳入的apply(用于實(shí)際的方法調(diào)用)包裝成(target) -> () -> ()
        invoking(signature: signature) { (target: Any) in
            return {
                // 利用as保證類型安全,T為具體的協(xié)議類型
                (target as? T).map(apply)
            }
        }
    }
  
    /** 處理事件流的執(zhí)行吗讶、掛起燎猛、恢復(fù) */
    private func invoking(signature: String,
                          index: Int = 0,
                          handler: @escaping (Any) -> () -> ()) {
        // 迭代執(zhí)行事件流
        for idx in index..<targets.count {
            let target = targets[idx]
            // 查詢掛起、中斷狀態(tài)
            if let (isSuspend, isInterrupt) = suspendOfEvents[signature] {
                if isInterrupt {
                    // 事件流中斷
                    print("cancel \(signature) at \(type(of: target))")
                    break
                } else if isSuspend {
                    // 掛起事件流程
                    resumeOfEvents[signature] = (idx, handler)
                    print("suspend \(signature) at \(type(of: target))")
                    continue
                }
            }
            
            // 執(zhí)行事件流
            handler(target)()
        }
    }
    
    /** 刪除執(zhí)行中的事件流 */
    private func removeSuspendEvent(signature: String) {
        if suspendOfEvents.keys.contains(signature) {
            resumeOfEvents.removeValue(forKey: signature)
            suspendOfEvents.removeValue(forKey: signature)
        }
    }
}

簡(jiǎn)單說(shuō)明一下:

  1. invoke為proxy的通用執(zhí)行方法照皆,通過(guò)#function獲取方法簽名
  2. invoking用于處理事件流重绷,target在具體回調(diào)時(shí)可以通過(guò)suspend來(lái)控制是否允許后續(xù)的事件流掛起或者終止
  3. resume方法因?yàn)樾枰獋魅雜ignature比較麻煩,所以在suspend中通過(guò)currying包裝resume簡(jiǎn)化外部使用

下面為測(cè)試代碼 —— 協(xié)議與Targets部分:

// 代理實(shí)例
var proxy = Proxy()
// 用于測(cè)試的協(xié)議
protocol Testable {
    func say()
    func hello(name: String)
}
// 為Testable協(xié)議提供默認(rèn)的實(shí)現(xiàn)
extension Testable {
    func say() {
        print("Testable say()")
    }
    func hello(name: String) {
        print("Testable hello(name:)")
    }
}

struct Boo : Testable {
    func say() {
        print("boo say()")
        // 掛起事件流
        proxy.suspend()
    }
}

struct Foo : Testable {
    func say() {
        print("Foo say()")
    }
    
    func hello(name: String) {
        print("foo hello \(name)")
    }
}
// 向代理添加target
proxy.addTarget(target: Boo())
proxy.addTarget(target: Foo())

下面為測(cè)試代碼 —— 事件源部分:

/** 用于模擬的事件源 */
struct EventsSource {
    let proxy: Proxy
}

/** 分組協(xié)議 */
protocol Groupable { }

/** 實(shí)現(xiàn)「分組」協(xié)議  */
extension EventsSource : Groupable {  }

/** 「事件回調(diào)」分組 */
struct Eventer<Subject: Groupable> {
    var subject: Subject
    init(subject: Subject) {
        self.subject = subject
    }
}
extension Groupable {
    var events: Eventer<Self> {
        get {
            return Eventer(subject: self)
        }
    }
}

/** 包裝proxy的通用invoke */
extension Eventer where Subject == EventsSource {
    func say() {
        // target: Testable用于讓編譯器推斷泛型類型
        self.subject.proxy.invoke { (target: Testable) in
            target.say()
        }
    }
    
    func hello(name: String) {
        // 因?yàn)闊o(wú)法直接獲得原方法的簽名膜毁,所以要用(T) -> ()進(jìn)行currying
        self.subject.proxy.invoke { (target: Testable) in
            target.hello(name: name)
        }
    }
}

上面的測(cè)試代碼在定義EventsSource后昭卓,使用曾經(jīng)分享過(guò)的「分組」技巧來(lái)包裝Proxy中的invoke方法愤钾。

let source = EventsSource(proxy: proxy)
print(">>> call say ...")
source.events.say()
print(">>> call hello ...")
source.events.hello(name: "fh")
print(">>> resume say ...")
proxy.resume(signature: "say()")

// 執(zhí)行結(jié)果如下:
>>> call say ...
boo say()
suspend say() at Foo
>>> call hello ...
Testable hello(name:)
foo hello fh
>>> resume say ...
Foo say()
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市候醒,隨后出現(xiàn)的幾起案子能颁,更是在濱河造成了極大的恐慌,老刑警劉巖倒淫,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伙菊,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡敌土,警方通過(guò)查閱死者的電腦和手機(jī)镜硕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)返干,“玉大人兴枯,你說(shuō)我怎么就攤上這事【厍罚” “怎么了财剖?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)癌淮。 經(jīng)常有香客問(wèn)我峰伙,道長(zhǎng),這世上最難降的妖魔是什么该默? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任瞳氓,我火速辦了婚禮,結(jié)果婚禮上栓袖,老公的妹妹穿的比我還像新娘匣摘。我一直安慰自己,他們只是感情好裹刮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開(kāi)白布音榜。 她就那樣靜靜地躺著,像睡著了一般捧弃。 火紅的嫁衣襯著肌膚如雪赠叼。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,046評(píng)論 1 285
  • 那天违霞,我揣著相機(jī)與錄音嘴办,去河邊找鬼。 笑死买鸽,一個(gè)胖子當(dāng)著我的面吹牛涧郊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播眼五,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼妆艘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彤灶!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起批旺,我...
    開(kāi)封第一講書(shū)人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤幌陕,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后汽煮,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體搏熄,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年逗物,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瑟俭。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡翎卓,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出摆寄,到底是詐尸還是另有隱情失暴,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布微饥,位于F島的核電站逗扒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏欠橘。R本人自食惡果不足惜矩肩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肃续。 院中可真熱鬧黍檩,春花似錦、人聲如沸始锚。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)瞧捌。三九已至棵里,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間姐呐,已是汗流浹背殿怜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留曙砂,地道東北人稳捆。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像麦轰,于是被迫代替她去往敵國(guó)和親乔夯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子砖织,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

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

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒(méi)有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,089評(píng)論 1 32
  • 參考資源《swifter》https://github.com/iOS-Swift-Developers/Swif...
    柯浩然閱讀 1,427評(píng)論 0 6
  • 基礎(chǔ) 1. 為什么說(shuō)Objective-C是一門動(dòng)態(tài)的語(yǔ)言? 2. 講一下MVC和MVVM末荐,MVP侧纯? 3. 為...
    波妞和醬豆子閱讀 3,307評(píng)論 0 46
  • 畢業(yè)將至,書(shū)本太多太重甲脏,沒(méi)辦法搬回家眶熬。有些書(shū)或許未來(lái)會(huì)有大用,雖然搬不走块请,但能記錄下來(lái)娜氏,以后在網(wǎng)上再買。 大學(xué)物理...
    Manegga閱讀 153評(píng)論 0 0
  • 辦公室里有一位大叔墩新,四十多歲的樣子贸弥,我剛來(lái)的一周里,他就給我留下了深刻的印象海渊。 他每日必喝兩次咖啡绵疲,雖然很多人把喝...
    妞妞先生閱讀 325評(píng)論 1 1