如果你的工程是采用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依旧是一个很优雅的组件化方案。因为:
- CTMediator是一套不需要随工程迭代修改的代码,它也不与任何业务产生交互,引入之后对Swift工程开发的影响为0
- Swift工程可以使用Extension替代原先Objective-C工程的Category并能够正常发版。因此在方案实施全程都可以做到纯Swift编码,可以完全忘记CTMediator是Objective-C工程这回事儿
- 新版的CocoaPods对依赖Objective-C Pod的Swift Pod十分友好,在Swift Pod中引入CTMediator可以做到完全无感,你即使不会写Objective-C,也不影响你来使用CTMediator方案
- Swift工程也可以使用Objective-C工程历史遗留的Category和Target-Action来完成调度,因此Objective-C开发团队可以使用旧有代码渐进地实施Swift迁移,而不必担心引入新的bug。
综合以上几点,可以给到2个结论:
- CTMediator方案是可以优雅地应用在Swift工程中的
- 凡是过去应用CTMediator组件化方案的Objective-C团队都可以做到无痛迁移Swift
前面的部分论证了结论1
。
文章接下来的内容有两个目的:
- 说明
结论2
。因此之前就采用CTMediator组件化方案的Objective-C团队们,你们看完这篇文章之后,就可以放心地让团队往Swift方向迁移了。 - 一直在做Swift开发的团队们,你们也可以放心使用CTMediator方案了。它不仅更加强大,同时
CTMediator是Objective-C工程
这一点对Swift工程(无论是现有工程还是新工程)的影响为0
- 为Swift响应者组件提供Target-Action
- 为方便调度给CTMediator写Extension
- Swift响应者工程、Swift调度者工程、Extension工程的发版
- Swift调用者通过Extension调度Objective-C响应者
- 总结: 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的注意事项
- Target对象必须要继承自
NSObject
- Action方法必须带
@objc
前缀 - 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的注意事项
- 写Extension的人需要知道响应者的module名,并且在传入的参数字典中给到,例如:
[kCTMediatorParamsKeySwiftTargetModuleName:"module_name"]
,这是Swift与Objective-C不同的地方。如果响应者是Swift,不给到Module名的话,runtime是调度不到响应者target-action的。如果是Objective-C的响应者,这个就可以省略了。 - cocoapods发版的时候要带
--use-modular-headers
,因为这个组件依赖了CTMediator
,它是Objective-C的工程。--use-modular-headers
这个参数在Cocoapods 1.6.0.beta.1
以上的版本才有。 - 如果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响应者
- Extension中给到的params需要带有
kCTMediatorParamsKeySwiftTargetModuleName
key来给到target所在module的名字 - Extension中不需要针对closure做任何转化
- 响应者target-action跟正常情况一样写
- 响应者action方法首参数不要带Argument Label,用
_
2 Swift调用者 + Extension + Objective-C响应者
- Extension中给到的params不需要带
kCTMediatorParamsKeySwiftTargetModuleName
key - Extension中需要将closure转化成block对象之后再放入params传过去
- 响应者的Target-Action跟正常情况一样写
3 Swift调用者 + Category + Swift响应者
- Category中给到的params需要带有
kCTMediatorParamsKeySwiftTargetModuleName
key来给到target所在module的名字 - Category中不需要针对block做任何转化
- 响应者的Target-Action需要将block转化成closure
- 响应者action方法首参数不要带Argument Label,用
_
4 Swift调用者 + Category + Objective-C响应者
- Category中给到的params不需要带
kCTMediatorParamsKeySwiftTargetModuleName
key - Category中不需要针对block做任何转化
- 响应者的Target-Action跟正常情况一样写
5 Objective-C调用者 + Category + Objective-C响应者
- Category中给到的params不需要带
kCTMediatorParamsKeySwiftTargetModuleName
key - Category中不需要针对block做任何转化
- 响应者的Target-Action跟正常情况一样写
6 Objective-C调用者 + Category + Swift响应者
- Category中给到的params需要带有
kCTMediatorParamsKeySwiftTargetModuleName
key来给到target所在module的名字 - Category中不需要针对block做任何转化
- 响应者的Target-Action需要将block转化成closure
- 响应者action方法首参数不要带Argument Label,用
_
7 Objective-C调用者 + Extension + Objective-C响应者
- Extension中给到的params不需要带
kCTMediatorParamsKeySwiftTargetModuleName
key - Extension中需要将closure转化成block对象之后再放入params传过去
- Extension中的方法需要带前缀@objc
- 响应者的Target-Action跟正常情况一样写
8 Objective-C调用者 + Extension + Swift响应者
- Extension中给到的params需要带有
kCTMediatorParamsKeySwiftTargetModuleName
key来给到target所在module的名字 - Extension中不需要针对closure做任何转化
- Extension中的方法需要带前缀@objc
- 响应者target-action跟正常情况一样写
- 响应者action方法首参数不要带Argument Label,用
_
最后给到示例工程:
Objective-C为调用者:ModulizedMainProject
Swift为调用者:SwfitDemo
使用前先添加私有仓库:
pod repo add PrivatePods https://github.com/ModulizationDemo/PrivatePods.git
然后进入对应工程pod update --verbose
即可。
关于CTMediator还有什么内容我没讲,或者没讲清楚的,再或者你针对CTMediator方案有任何问题的,都可以在文章下方评论区向我提问。
Comments
comments powered by Disqus