这是我参与8月更文挑战的第26天,活动详情查看: 8月更文挑战
iOS组件化使用
一、实现方案
目前组件化方案大体分为两种URL/protocol注册调度和runtime调度
注册调度的方案存在以下两个问题:
- 命名域渗透
- 注册是不必要的,因为我们需要维护注册列表,这也是一部分成本
故而本文选择runtime调度的方案来实现组件化;也就是采用CTMediator的Target_Action组件化方案实现;CTMediator的方案在支持Objective-C组件调用的基础上,扩展支持Swift组件,可以达到混合调用的目的,组件的开发可以使用Objective-C或者Swift语言;
CTMediator方案的表象是通过runtime调度Target-Action,但是CTMediator方案的本质是在不需要动业务代码的情况下,完成调度。
所以在提供Target-Action的时候,我们一般都选择让Action把对应的业务做完
Swift工程声明Target-Action的注意事项:
Target对象必须要继承自NSObjectAction方法必须带@objc前缀Action方法第一个参数不能有Argument Label
二、引入中间件
pod 'CTMediator', '~> 25'
三、使用(以登录Login模块为例)
Mediator文件夹存放Login模块对外开放的中间件
Target_Login文件即为Target_Action方案的具体实现,也是调用登录模块功能的实现
CTMediator+Login文件即为Target_Action方案对外开放的接口,也是外部模块调用Login模块时可使用的方法。
两个类的后缀
Login可以不必保持一致,但需要在内部定义时指定好对应的映射关系,此处为了便于理解,将后缀保持一致。
四、代码解析
1、Target_Login
Login模块对外暴露的接口类CTMediator+Login中方法的具体实现
class Target_Login: NSObject {
/// 获取登录视图控制器
/// - Parameter sender: 参数
/// - Returns: 返回控制器
@objc func Action_getLoginVC(_ sender: NSDictionary) -> UIViewController {
let login = LoginViewController()
return login
}
}
Swift工程声明Target-Action的注意事项
Target对象必须要继承自NSObject;Action方法必须带@objc前缀;- 响应者
action方法首参数不要带Argument Label,用_; - 响应者的
Target-Action需要将block转化成closure;
2、CTMediator+Login
此类为对外开发的可调用的接口,也是中间件CTMediator的类别
其内部实现为:
import CTMediator
let kCTMediatorTarget_Login = "Login";//对应Target_Login中的Login
let kCTMediatorAction_getLoginVC = "getLoginVC";//对应Target_Login中的方法名Action_getLoginVC后半段
extension CTMediator {
/// 获取登录视图控制器
/// - Returns: 返回控制器
func mediator_getLoginVC() -> UIViewController {
let loginVC: UIViewController = performTarget(kCTMediatorTarget_Login, action: kCTMediatorAction_getLoginVC, params: [kCTMediatorParamsKeySwiftTargetModuleName:AppInfo.appDisplayName], shouldCacheTarget: false) as! UIViewController
return loginVC
}
}
调用
swift项目模块需要添加参数kCTMediatorParamsKeySwiftTargetModuleName,对应为工程名字
其中 kCTMediatorTarget_Login对应的字符串Login即为Target_Login类对应的后半部分;
kCTMediatorAction_getLoginVC对应的字符串getLoginVC即为Target_Login类对应的方法Action_getLoginVC后半部分;
此处的方法名
mediator_getLoginVC跟上述定义的字符串并无关系,为了便于理解方法对应关系,起了一样的名字
我们来看一下- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget方法的具体实现:
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
// generate target
NSString *targetClassString = nil;
if (swiftModuleName.length > 0) {
targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
} else {
targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
}
NSObject *target = self.cachedTarget[targetClassString];
if (target == nil) {
Class targetClass = NSClassFromString(targetClassString);
target = [[targetClass alloc] init];
}
// generate action
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
SEL action = NSSelectorFromString(actionString);
if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
if (shouldCacheTarget) {
self.cachedTarget[targetClassString] = target;
}
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
return [self safePerformAction:action target:target params:params];
} else {
// 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
[self.cachedTarget removeObjectForKey:targetClassString];
return nil;
}
}
}
在CTMediator的实现中,会根据这些定义的字符串通过运行时机制拼接成对应的Target_Login及对应的方法Action_getLoginVC并调用。
五、使用及解耦思路
1、使用方法:
let login = CTMediator.sharedInstance()?.mediator_getLoginVC()
获取登录控制器界面,然后进行操作。
2、解耦思路
- 只需要在
Target_Login中对自身模块产生依赖,但Target_Login自身并不暴露与外部 - 外部通过调用
CTMediator+Login来实现对Target_Login的调用,但类CTMediator+Login与Target_Login并不直接产生依赖关系,而是通过运行时实现内部依赖。 - 去model化,模块之前通过
NSDictionary传值
六、其他
1、特殊类型传值
以三级区域选择列表为例,演示闭包传值:
/// 选择区域
func selectAreaAction() {
let area = CTMediator.sharedInstance()?.mediator_getAreaVC(callBack: { (dict) in
})
area!.hidesBottomBarWhenPushed = true
navigationController?.pushViewController(area!, animated: true)
}
其中callBack即为选中区域之后,返回时的闭包传值,传回选中的区域信息。此闭包最终需要通过中间件绑定在三级区域选择模块的实现类上,那么如何操作:
1> 将闭包放在NSDictionary中,通过中间件传递给区域选择类
let kCTMediatorTarget_Area = "Area";//对应Target_Area中的Area
let kCTMediatorAction_getAreaVC = "getAreaVC";//对应Target_Area中的方法名Action_getAreaVC后半段
extension CTMediator {
func mediator_getAreaVC(callBack: @escaping (NSDictionary) -> Void) -> UIViewController {
let params = [kCTMediatorParamsKeySwiftTargetModuleName:AppInfo.appDisplayName, "callBack": callBack] as [AnyHashable : Any]
if let loginVC = performTarget(kCTMediatorTarget_Area,
action: kCTMediatorAction_getAreaVC,
params: params,
shouldCacheTarget: false) as? UIViewController {
return loginVC
}
return CTMErrorViewController()
}
}
kCTMediatorParamsKeySwiftTargetModuleName此参数为swift调用的命名空间,对应工程名字
2> 将闭包绑定于区域选择类
class Target_Area: NSObject {
/// 获取区域选择视图控制器
/// - Parameter sender: 参数
/// - Returns: 返回控制器
@objc func Action_getAreaVC(_ data: NSDictionary) -> UIViewController {
typealias CallBack = (_ dictionary: NSDictionary) -> Void
let callBack: CallBack = data.object(forKey: "callBack") as! CallBack
let area = SelectAreaController()
area.selectedBlock = {(dict) in
callBack(dict)
}
return area
}
}