如果你的工程是采用CTMediator方案做的组件化,看完本文以后,你就可以做到渐进式地迁移到Swift了。 CTMediator支持所有情况的调用,具体可以看文后总结。你的工程可以让Swift组件和Objective-C组件通过CTMediator混合调用 也就是说:以后再开新的组件,可以直接用Swift来写,旧有代码不会收到任何影响。




这篇文章适用最低支持iOS 8的工程,且必须使用Cocoapods 1.6.0.beta.1以上的版本




本文提及的框架:CTMediator


相关文章:《iOS应用架构谈 组件化方案》 《在现有工程中实施基于CTMediator的组件化方案


Swift调度Demo工程:SwfitDemo,Objective-C调度Demo工程:ModulizedMainProject


跑Demo前先添加私有仓库:


pod repo add PrivatePods https://github.com/ModulizationDemo/PrivatePods.git


然后进入对应工程pod update --verbose即可。




最近我惊喜地发现,几天前(8月17号)Cocoapods在1.6.0.beta.1版提供了--use-modular-headers参数,它可以大大简化依赖Objective-C Pod的Swift Pod的发版流程。


于是我打算把我过去这些为Objective-C的工程写的框架全部用Swift再写一遍。目前已经完成的只有SwiftHandyFrame。(这个工具在我自己的业余项目里用了一圈,感觉很不错,达到了我之前设想的目的。)


然而以上开源工程中,有一个从设计思想上就非常依赖Objective-C的框架是CTMediator


我尝试去思考一个新的、符合Swift且优雅的组件化方案,但并没有找到比CTMediator更好或一样好的方案。于是我探索尝试了一下,也填了一些坑。最终我发现CTMediator方案本身虽然很不Swift,但即使是在Swift工程中,CTMediator依旧是一个很优雅的组件化方案。因为:


  1. CTMediator是一套不需要随工程迭代修改的代码,它也不与任何业务产生交互,引入之后对Swift工程开发的影响为0
  2. Swift工程可以使用Extension替代原先Objective-C工程的Category并能够正常发版。因此在方案实施全程都可以做到纯Swift编码,可以完全忘记CTMediator是Objective-C工程这回事儿
  3. 新版的CocoaPods对依赖Objective-C Pod的Swift Pod十分友好,在Swift Pod中引入CTMediator可以做到完全无感,你即使不会写Objective-C,也不影响你来使用CTMediator方案
  4. Swift工程也可以使用Objective-C工程历史遗留的Category和Target-Action来完成调度,因此Objective-C开发团队可以使用旧有代码渐进地实施Swift迁移,而不必担心引入新的bug。



综合以上几点,可以给到2个结论:


  1. CTMediator方案是可以优雅地应用在Swift工程中的
  2. 凡是过去应用CTMediator组件化方案的Objective-C团队都可以做到无痛迁移Swift



前面的部分论证了结论1


文章接下来的内容有两个目的:


  1. 说明结论2。因此之前就采用CTMediator组件化方案的Objective-C团队们,你们看完这篇文章之后,就可以放心地让团队往Swift方向迁移了。
  2. 一直在做Swift开发的团队们,你们也可以放心使用CTMediator方案了。它不仅更加强大,同时CTMediator是Objective-C工程这一点对Swift工程(无论是现有工程还是新工程)的影响为0




  1. 为Swift响应者组件提供Target-Action
  2. 为方便调度给CTMediator写Extension
  3. Swift响应者工程、Swift调度者工程、Extension工程的发版
  4. Swift调用者通过Extension调度Objective-C响应者
  5. 总结: Swift组件与Objective-C组件互相调用的全部8种情况,及其对策









1. 为Swift响应者组件提供Target-Action



Target-Action 的目的



CTMediator方案的表象是通过runtime调度Target-Action,但是CTMediator方案的本质是在不需要动业务代码的情况下,完成调度


所以在提供Target-Action的时候,我们一般都选择让Action把对应的业务做完,如果有调用者需要补充逻辑的,通过closure给到。而不是返回一个什么对象给调用者,然后调用者再去做逻辑。


举个例子:


A需要展示B页面。你可以选择:

1. 让B的Target-Action直接返回一个UIViewController,然后A去push或者present。
2. 让B的Target-Action直接完成页面展示的操作,具体是push还是present,由A传过来的参数决定。

一个好的Target-Action倾向于2。因为这种做法对于A业务来说,留下的足迹更小,踏雪无痕是我们追求的目标。




Swift工程声明Target-Action的注意事项



  1. Target对象必须要继承自NSObject
  2. Action方法必须带@objc前缀
  3. Action方法第一个参数不能有Argument Label



一个Swift版的Target-Action如下:


class Target_A: NSObject { // 必须要继承自NSObject

    // 正确的Action声明
    @objc func Action_viewController(_ params:[AnyHashable:Any]?) -> UIViewController {

    }

    // 错误的Action声明:没有带@objc前缀
    func Action_viewController(_ params:[AnyHashable:Any]?) -> UIViewController {

    }

    // 错误的Action声明:方法带上了Argument Label
    func Action_viewController(viewControllerParams params:[AnyHashable:Any]?) -> UIViewController {

    }

    // 错误的Action声明:方法带上了Argument Label
    func Action_viewController(params:[AnyHashable:Any]?) -> UIViewController {

    }

}


params 的类型也可以为NSDictionary,所以这么写也是可以的:


    func Action_viewController(params:NSDictionary) -> UIViewController {

    }




Swift工程实现Action时的注意事项



这里要注意的点在于如何处理block或closure。主要原因是如果调用者通过Category来发起调用,那么就只能传递block。如果是通过Extension来发起调用,那么就只能传递closure。至于调用者是Swift还是Objective-C倒是无所谓的。


  • 我们先看调用者是通过Category发起的调用,然后响应者是Swift的情况


由于Category中只能传递block,所以此时你的Action获得了一个block。在swift响应者中,这个Action就要这么写:


    @objc func Action_viewController(_ params:[AnyHashable:Any]?) -> UIViewController {

        if let actionParams = params {
            let block = actionParams["callback"]

            // 转换一下
            typealias CallbackType = @convention(block) (String) -> Void
            let blockPtr = UnsafeRawPointer(Unmanaged<AnyObject>.passUnretained(block as AnyObject).toOpaque())
            let callback = unsafeBitCast(blockPtr, to: CallbackType.self)

            // 此时block就变成了closure,就可以正常调用了
            callback("success") 
        }

        let aViewController = ViewController()
        return aViewController
    }




  • 我们再看调用者通过Extension发起的调用,然后响应者是Swift的情况


这种情况就很自然了,因为两边都是Swift环境(对的,我们完全可以忽略CTMediator是一个Objective-C组件的事实),所以可以直接使用:


    @objc func Action_viewController(_ params:[AnyHashable:Any]?) -> UIViewController {

        if let actionParams = params {
            if let callback = actionParams["callback"] as? (String) -> Void {
                callback("success")
            }
        }

        let aViewController = ViewController()
        return aViewController

    }


所以如果你的响应者需要同时服务来自Category的调度和Extension的调度,而且需要处理block或closure的话,你就需要在Category中或Extension中给到一个参数,来决定你如何实现这个Action。不过一般来说这种情况很少,为了同一个调度又写Category又写Extension基本上是不太可能的。







2. 为方便调度给CTMediator写Extension



Extension 的目的



理论上我们可以直接使用CTMediator来完成调度。但是这么做的话,写调用代码的工程师就会产生这样的迷惑:“为了调度成功,我应该给到什么target,什么action,以及参数都要传哪些?”


// 如果直接这么使用CTMediator,调用工程师需要去查文档获知自己这次调用对应的target-action是什么,参数是什么,module_name是什么。
    CTMediator.sharedInstance.performTarget("A",action: "viewController", params: ["name":"casa", "age":18, kCTMediatorParamsKeySwiftTargetModuleName:"module_name"], shouldCacheTarget: false)


所以为了不让写代码的工程师迷惑,我们提供Extension来描述一个调用应该传什么样的参数。在Extension的实现中,我们写入Target、Action以及ModuleName,这样工程师就不必迷惑了。


// extension CTMediator
    public func A_show(callback:@escaping (String) -> Void) -> UIViewController?

// 给CTMediator写了extension之后,调用工程师拿到方法,按照方法的参数列表给到参数就可以了,不必去考虑对应的target-action是什么、参数是什么、module_name是什么了。
    let acontroller = CTMediator.sharedInstance().A_show { (result) in
        print(result)
    }


写Extension的注意事项



  1. 写Extension的人需要知道响应者的module名,并且在传入的参数字典中给到,例如:[kCTMediatorParamsKeySwiftTargetModuleName:"module_name"],这是Swift与Objective-C不同的地方。如果响应者是Swift,不给到Module名的话,runtime是调度不到响应者target-action的。如果是Objective-C的响应者,这个就可以省略了。
  2. cocoapods发版的时候要带--use-modular-headers,因为这个组件依赖了CTMediator,它是Objective-C的工程。--use-modular-headers这个参数在Cocoapods 1.6.0.beta.1以上的版本才有。
  3. 如果Extension里的某个方法要被Objective-C使用,那需要带上前缀@objc



一个完整可行的Extension是这样的:


import CTMediator

extension CTMediator {
    // 如果这个方法也要给Objective-C工程调用,就需要加上@objc
    @objc public func A_show(callback:@escaping (String) -> Void) -> UIViewController? {
        let params = [
            "callback":callback,
            kCTMediatorParamsKeySwiftTargetModuleName:"A_swift" // 需要给到module名
            ] as [AnyHashable : Any]
        if let viewController = self.performTarget("A", action: "viewController", params: params, shouldCacheTarget: false) as? UIViewController {
            return viewController
        }
        return nil
    }
}


它的发版命令是这样的:


// 带上--use-modular-headers,Cocoapods 1.6.0.beta.1以上支持
pod repo push Your_Repository Your_Podspec_file.podspec --verbose --allow-warnings --use-libraries --use-modular-headers







3. Swift响应者工程、Swift调度者工程、Extension工程的发版



其实跟之前私有pod的发版流程一模一样,只是如果你的Swift工程依赖了Objective-C工程的话,你多带一个--use-modular-headers参数而已。







4. Swift调用者通过Extension调度Objective-C响应者



这种情况下要注意的其实还是只有block和closure之间的转化,Extension需要将Swift的Closure转化成Objective-C的block对象之后再传递,完整的Extension示例如下:


extension CTMediator {
    public func A_showObjc(callback:@escaping (String) -> Void) -> UIViewController? {

        // 将closure类型转化为block类型
        let callbackBlock = callback as @convention(block) (String) -> Void
        let callbackBlockObject = unsafeBitCast(callbackBlock, to: AnyObject.self)

        // 转化完毕就可以放入params中传递了
        let params = ["callback":callbackBlockObject] as [AnyHashable:Any]

        if let viewController = self.performTarget("A", action: "viewController", params: params, shouldCacheTarget: false) as? UIViewController {
            return viewController
        }
        return nil
    }
}




对应的响应者Target-Action如下:


- (UIViewController *)Action_viewController:(NSDictionary *)params
{
    typedef void (^CallbackType)(NSString *);
    CallbackType callback = params[@"callback"];
    if (callback) {
        callback(@"success");
    }
    AViewController *viewController = [[AViewController alloc] init];
    return viewController;
}


这样就能保证调用成功。







5. 总结: Swift组件与Objective-C组件互相调用的全部8种情况,及其对策



其实如果你的工程是Swift调用者、Swift响应者的情况,那么应用CTMediator就一点问题都没有。


对于Objective-C的开发团队来说,如果开始渐进地将工程Swift化,那么就需要分各种情况去处理block和closure转化的问题。在这里我把可能出现的所有情况及其处理方法总结如下:




1 Swift调用者 + Extension + Swift响应者



  1. Extension中给到的params需要带有kCTMediatorParamsKeySwiftTargetModuleNamekey来给到target所在module的名字
  2. Extension中不需要针对closure做任何转化
  3. 响应者target-action跟正常情况一样写
  4. 响应者action方法首参数不要带Argument Label,用_




2 Swift调用者 + Extension + Objective-C响应者



  1. Extension中给到的params不需要带kCTMediatorParamsKeySwiftTargetModuleNamekey
  2. Extension中需要将closure转化成block对象之后再放入params传过去
  3. 响应者的Target-Action跟正常情况一样写




3 Swift调用者 + Category + Swift响应者



  1. Category中给到的params需要带有kCTMediatorParamsKeySwiftTargetModuleNamekey来给到target所在module的名字
  2. Category中不需要针对block做任何转化
  3. 响应者的Target-Action需要将block转化成closure
  4. 响应者action方法首参数不要带Argument Label,用_




4 Swift调用者 + Category + Objective-C响应者



  1. Category中给到的params不需要带kCTMediatorParamsKeySwiftTargetModuleNamekey
  2. Category中不需要针对block做任何转化
  3. 响应者的Target-Action跟正常情况一样写




5 Objective-C调用者 + Category + Objective-C响应者



  1. Category中给到的params不需要带kCTMediatorParamsKeySwiftTargetModuleNamekey
  2. Category中不需要针对block做任何转化
  3. 响应者的Target-Action跟正常情况一样写




6 Objective-C调用者 + Category + Swift响应者



  1. Category中给到的params需要带有kCTMediatorParamsKeySwiftTargetModuleNamekey来给到target所在module的名字
  2. Category中不需要针对block做任何转化
  3. 响应者的Target-Action需要将block转化成closure
  4. 响应者action方法首参数不要带Argument Label,用_




7 Objective-C调用者 + Extension + Objective-C响应者



  1. Extension中给到的params不需要带kCTMediatorParamsKeySwiftTargetModuleNamekey
  2. Extension中需要将closure转化成block对象之后再放入params传过去
  3. Extension中的方法需要带前缀@objc
  4. 响应者的Target-Action跟正常情况一样写




8 Objective-C调用者 + Extension + Swift响应者



  1. Extension中给到的params需要带有kCTMediatorParamsKeySwiftTargetModuleNamekey来给到target所在module的名字
  2. Extension中不需要针对closure做任何转化
  3. Extension中的方法需要带前缀@objc
  4. 响应者target-action跟正常情况一样写
  5. 响应者action方法首参数不要带Argument Label,用_




最后给到示例工程:


Objective-C为调用者:ModulizedMainProject


Swift为调用者:SwfitDemo


使用前先添加私有仓库:

pod repo add PrivatePods https://github.com/ModulizationDemo/PrivatePods.git


然后进入对应工程pod update --verbose即可。




关于CTMediator还有什么内容我没讲,或者没讲清楚的,再或者你针对CTMediator方案有任何问题的,都可以在文章下方评论区向我提问。




评论系统我用的是Disqus,不定期被墙。所以如果你看到文章下面没有加载出评论列表,翻个墙就有了。




本文遵守CC-BY。 请保持转载后文章内容的完整,以及文章出处。本人保留所有版权相关权利。

我的博客拒绝挂任何广告,如果您觉得文章有价值,可以通过支付宝扫描下面的二维码捐助我。


Comments

comments powered by Disqus