美文网首页Swift编程
使用纯Swift编写一个事件代理

使用纯Swift编写一个事件代理

作者: fuadam1982 | 来源:发表于2019-07-02 00:07 被阅读29次

    在日常开发中我们经常会遇到这样的场景,有很多模块的delegate需要通过一个公共类来转发回调事件。比如采用MVP模式开发一个复杂的UI交互,其中(许多)View要通过Presenter来转发网络回调、文件访问、数据库等多种不同的Delegate的回调事件。标准做法是在Presenter中实现各个不同的Delegate,然后再转发给(多个)View。这样做繁琐且代码重复,更重要的是不方便对多个View做回调控制。例如:用户在首次观看视频时,点击「退出」按钮会响应「视频退出事件」此时会弹出一个confirm,如果选择「取消」则中断后续的事件回调。对于这样的场景使用proxy是最方便的。如果有这么一个Proxy,则Presenter不需要转发一次事件回调,而只要将(多个)view添加到proxy接收Delegate的回调就可以了。如果用ObjeC来实现proxy是非常简单的,只需要实现三个runtime方法即可:

    /** 判断是否可以响应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;
    }
    
    /** 消息转发 */
    - (void)forwardInvocation:(NSInvocation *)invocation {
        for (id target in targets) {
            // 这里可添加更多的「控制」
            [context invokeWithTarget:target];
        }
    }
    

    但是对于pure swift来说,因为动态能力几乎为零——使用Mirror只能做些简单的属性操作而不能对方法做反射操作,所以要实现类似上面ObjC版的proxy功能需要解决三个问题:

    1. pure swift中的protocol没有@optional,也即没有respondsToSelector的能力
    2. proxy是一个通用的实现,而delegate中的方法签名是各不相同的,也即没有@selector的能力
    3. swift是泛型+类型推导的编程范式,而各回调方法的参数类型是完全不同的。另外proxy内部维护target列表的类型必然是Any——也就是无类型。这就导致无法通过「as」强转出原方法签名并执行,也即没有invocation等能力

    因为上述的三个理由,在pure swift的实现版本中代码冗余一些是必然的但至少能实现。下面我们来逐个解决上面列出的问题:

    1. 对于第一个问题,所有的静态语言都只能用继承(多态)来解决。还好swift的extension(或者其它支持类mixin的静态语言)可以借助编译器做到多态而又不会侵入到target中(所有单继承的OO语言,对于继承是即爱又恨)
    2. 第二个问题的标准做法还是继承,通过一个proxy的子类实现所有会用到的delegate的协议。在swift中则是通过extension来包装proxy的「通用」invoke方法。这个方式可以很方便的解决方法签名的强类型校验,但缺点是delegate中的每一个方法都需要包装一遍,其实这只能算是强类型语言的特点不能算缺陷。另外还可以用swift 5.0中的@dynamicCallable + @dynamicmemberlookup实现对delegate中方法的访问「动态」访问,只不过实现起来还会更繁琐一些
    3. 第三个问题与第二个问题是紧密相连的,如果采用@dynamicCallable + @dynamicmemberlookup方式则可以使用([String : Any?]) -> (Instance) -> ()对delegate的各方法做currying。如果采用包装proxy的「通用」invoke方法的实现方式则可以通过(T) -> ()对个方法做currying,其中T为delegate协议类型。

    结合下面的代码会更容易理解上面的解释:

    /**
     *  通过事件代理
        TODO:
        1. 并发控制
        2. 设定target响应优先级
        3. 对target设定所在的回调线程
     */
    class Proxy {
        /** 缓存转发的target列表 */
        private var targets: [Any] = []
        /** 记录事件流挂起/终止情况 */
        private var suspendOfEvents: [String: (Bool, Bool)] = [:]
        /** 记录恢复事件流时对应target的Index和需要执行的回调closure */
        private var resumeOfEvents: [String: (Int,  (Any) -> () -> ())] = [:]
        
        /** 添加目标对象 */
        func addTarget(target: Any) {
            targets.append(target)
        }
        
        /** 挂起事件流 */
        @discardableResult
        func suspend(signature: String = #function,
                     isInterrupt: Bool = false) -> ((Bool) -> ())? {
            if suspendOfEvents.keys.contains(signature) {
                // 标记挂起
                suspendOfEvents[signature] = (true, isInterrupt)
                // 将resume包装成(isInterrupt) -> (),便于外部使用
                return {
                    [weak self] isInterrupt in
                    if let self = self {
                        self.resume(signature: signature, isInterrupt: isInterrupt)
                    }
                }
            } else {
                return nil
            }
        }
        
        /** 恢复事件流 */
        func resume(signature: String, isInterrupt: Bool = false) {
            if let (idx, handler) = resumeOfEvents[signature] {
                // 标记恢复
                suspendOfEvents[signature] = (false, isInterrupt)
                // 继续执行后续的事件流
                invoking(signature: signature, index: idx, handler: handler)
            }
        }
        
        /** 通用的事件回调执行器 */
        func invoke<T>(signature: String = #function,
                       apply: @escaping (T) -> ()) {
            // 删除未执行完的事件流
            removeSuspendEvent(signature: signature)
            // 记录执行情况
            suspendOfEvents[signature] = (false, false)
            // 执行事件流,将外部传入的apply(用于实际的方法调用)包装成(target) -> () -> ()
            invoking(signature: signature) { (target: Any) in
                return {
                    // 利用as保证类型安全,T为具体的协议类型
                    (target as? T).map(apply)
                }
            }
        }
      
        /** 处理事件流的执行、挂起、恢复 */
        private func invoking(signature: String,
                              index: Int = 0,
                              handler: @escaping (Any) -> () -> ()) {
            // 迭代执行事件流
            for idx in index..<targets.count {
                let target = targets[idx]
                // 查询挂起、中断状态
                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
                    }
                }
                
                // 执行事件流
                handler(target)()
            }
        }
        
        /** 删除执行中的事件流 */
        private func removeSuspendEvent(signature: String) {
            if suspendOfEvents.keys.contains(signature) {
                resumeOfEvents.removeValue(forKey: signature)
                suspendOfEvents.removeValue(forKey: signature)
            }
        }
    }
    

    简单说明一下:

    1. invoke为proxy的通用执行方法,通过#function获取方法签名
    2. invoking用于处理事件流,target在具体回调时可以通过suspend来控制是否允许后续的事件流挂起或者终止
    3. resume方法因为需要传入signature比较麻烦,所以在suspend中通过currying包装resume简化外部使用

    下面为测试代码 —— 协议与Targets部分:

    // 代理实例
    var proxy = Proxy()
    // 用于测试的协议
    protocol Testable {
        func say()
        func hello(name: String)
    }
    // 为Testable协议提供默认的实现
    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())
    

    下面为测试代码 —— 事件源部分:

    /** 用于模拟的事件源 */
    struct EventsSource {
        let proxy: Proxy
    }
    
    /** 分组协议 */
    protocol Groupable { }
    
    /** 实现「分组」协议  */
    extension EventsSource : Groupable {  }
    
    /** 「事件回调」分组 */
    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) {
            // 因为无法直接获得原方法的签名,所以要用(T) -> ()进行currying
            self.subject.proxy.invoke { (target: Testable) in
                target.hello(name: name)
            }
        }
    }
    

    上面的测试代码在定义EventsSource后,使用曾经分享过的「分组」技巧来包装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()")
    
    // 执行结果如下:
    >>> call say ...
    boo say()
    suspend say() at Foo
    >>> call hello ...
    Testable hello(name:)
    foo hello fh
    >>> resume say ...
    Foo say()
    

    相关文章

      网友评论

        本文标题:使用纯Swift编写一个事件代理

        本文链接:https://www.haomeiwen.com/subject/zlxwcctx.html