iOS 路由 组件化 Target-Action 方案(推荐)
Target-Action方案的优点。
- 充分的利用Runtime的特性,实现了组件间服务的自动发现,无需注册即可实现组件间的调用。
- Target-Action方案,只存在组件依赖Mediator中介的这一层依赖关系。
- 在每个组件中创建Mediator的Category分类,针对维护Mediator的Category分类即可。
- 每个组件的Category对应一个Target类,Categroy中的Action方法对应Target类中的Action场景。
- Target-Action方案统一了所有组件间调用入口。都是调用“performTarget: action: params: shouldCacheTarget:”方法。第三个参数是一个字典,这个字典里面可以传很多参数,只要Key-Value写好就可以了。
- Target-Action方案处理错误的方式也统一在一个地方了,Target没有,或者是Target无法响应的Action方法,都可以在Mediator这里进行统一出错处理。
- Target-Action方案也能有一定的安全保证,它对URL中的Native前缀进行安全验证。
- 因此,Target-Action方案不管从维护性、可读性、扩展性来说,都是一个比较完美的方案。
Target-Action方案的缺点。
- Target_Action在Category中将常规参数打包成字典,在Target处再把字典拆包成常规参数,这就造成了一部分的硬编码。
Target-Action路由方案的技术实现。
1、通过Target-Action的方式,创建一个Target类,Target类里面定义一些Action方法,这些方法的结果是返回一个Controller或其它Object对象。
2、在每个组件中,给Mediator中介类创建一个对应这个组件的分类,来保证Mediator中介类的代码纯净性。
3、在每个组件Mediator的分类中,定义组件外部可调用的接口方法。接口方法内部,通过统一调用Mediator中介类的“performTarget: action: params: shouldCacheTarget:”方法,实现组件间解耦。(即调用者即使不导入其它组件的头文件,也能调用其它组件。
4、在Mediator中介类的“performTarget: action: params: shouldCacheTarget:”方法中,主要通过Runtime中的NSClassFromString获取Target类,通过NSSelectorFromString获取Target类中的Action方法名。
5、路由方案最终实现是通过获取到的Target类和Target类中的Action方法名,去匹配已经创建好的Target类,由Target类中的Action方法,真正的实现创建需要的控制器或对象。将这些对象作为方法的返回值,传递给调用者。(即在Target类中的Action方法中,才能真正调用当前组件控制器的功能。)
相关链接
组件化架构漫谈
https://www.jianshu.com/p/67a6004f6930iOS应用架构谈 组件化方案
https://casatwy.com/iOS-Modulization.htmliOS中的三种路由方案实践
https://www.jianshu.com/p/72d705ecc177iOS 组件化 —— 路由设计思路分析
https://www.jianshu.com/p/76da56b3bd55
Target-Action组件化方案(来自casatwy)
整体架构
- casatwy组件化方案可以处理两种方式的调用,远程调用和本地调用,对于两个不同的调用方式分别对应两个接口。
- 远程调用通过AppDelegate代理方法传递到当前应用后,调用远程接口并在内部做一些处理,处理完成后会在远程接口内部调用本地接口,以实现本地调用为远程调用服务。
- 本地调用由performTarget:action:params:方法负责,但调用方一般不直接调用performTarget:方法。CTMediator会对外提供明确参数和方法名的方法,在方法内部调用performTarget:方法和参数的转换。
架构设计思路
casatwy是通过CTMediator类实现组件化的,在此类中对外提供明确参数类型的接口,接口内部通过performTarget方法调用服务方组件的Target、Action。由于CTMediator类的调用是通过runtime主动发现服务的,所以服务方对此类是完全解耦的。
但如果CTMediator类对外提供的方法都放在此类中,将会对CTMediator造成极大的负担和代码量。解决方法就是对每个服务方组件创建一个CTMediator的Category,并将对服务方的performTarget调用放在对应的Category中,这些Category都属于CTMediator中间件,从而实现了感官上的接口分离。
casatwy组件化实现细节对于服务方的组件来说,每个组件都提供一个或多个Target类,在Target类中声明Action方法。Target类是当前组件对外提供的一个“服务类”,Target将当前组件中所有的服务都定义在里面,CTMediator通过runtime主动发现服务。
在Target中的所有Action方法,都只有一个字典参数,所以可以传递的参数很灵活,这也是casatwy提出的去Model化的概念。在Action的方法实现中,对传进来的字典参数进行解析,再调用组件内部的类和方法。
架构分析
casatwy为我们提供了一个Demo,通过这个Demo可以很好的理解casatwy的设计思路,下面按照我的理解讲解一下这个Demo。
Demo链接
https://github.com/casatwy/CTMediator
文件目录
文件目录打开Demo后可以看到文件目录非常清楚,在上图中用蓝框框出来的就是中间件部分,红框框出来的就是业务组件部分。我对每个文件夹做了一个简单的注释,包含了其在架构中的职责。
远程调用和本地调用
在CTMediator中定义远程调用和本地调用的两个方法,其他业务相关的调用由Category完成。
// 远程App调用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地组件调用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
在CTMediator中定义的ModuleA的Category,为其他组件提供了一个获取控制器并跳转的功能,下面是代码实现。由于casatwy的方案中使用performTarget的方式进行调用,所以涉及到很多硬编码字符串的问题,casatwy采取定义常量字符串来解决这个问题,这样管理也更方便。
#import "CTMediator+CTMediatorModuleAActions.h"
NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";
@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail {
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:@{@"key":@"value"}];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品逻辑
return [[UIViewController alloc] init];
}
}
下面是ModuleA组件中提供的服务,被定义在Target_A类中,这些服务可以被CTMediator通过runtime的方式调用,这个过程就叫做发现服务。
在Target_A中对传递的参数做了处理,以及内部的业务逻辑实现。方法是发生在ModuleA内部的,这样就可以保证组件内部的业务不受外部影响,对内部业务没有侵入性。
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params {
// 对传过来的字典参数进行解析,并调用ModuleA内部的代码
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
命名规范
在大型项目中代码量比较大,需要避免命名冲突的问题。
对于这个问题casatwy采取的是加前缀的方式,从casatwy的Demo中也可以看出,其组件ModuleA的Target命名为Target_A,可以区分各个组件的Target。
被调用的Action命名为Action_nativeFetchDetailViewController:,可以区分组件内的方法与对外提供的方法。
casatwy将类和方法的命名,都统一按照其功能做区分当做前缀,这样很好的将组件相关和组件内部代码进行了划分。
CTMediator 代码注释
注释是自己对代码的理解,可能会有不对的地方,仅供参考。
调用路径
- 主框架调用模块的分类。
- 模块的分类调用统一的中间件CTMediator
- 中间件CTMediator通过runtime主动发现服务。
- 重点注释:在这里就实现了主框架对各个模块之间的解耦。(主框架不需要导入模块的头文件,也能实现对模块的调用)
- 中间件CTMediator调用模块Target中的Action方法。实现引入模块的功能。
- 模块将创建的控制器等需要实现的业务逻辑返回给主框架。
==主框架==
ViewController
//
// ViewController.h
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
//
// ViewController.m
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import "ViewController.h"
#import <HandyFrame/UIView+LayoutMethods.h>
#import "CTMediator+CTMediatorModuleAActions.h"
#import "TableViewController.h"
// 全局常量
NSString * const kCellIdentifier = @"kCellIdentifier";
@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) NSArray *dataSource;
@end
@implementation ViewController
#pragma mark - life cycle
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.tableView];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self.tableView fill];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.dataSource.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
cell.textLabel.text = self.dataSource[indexPath.row];
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// 取消选定由索引路径标识的给定行,并使用使取消选定具有动画效果的选项。
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (indexPath.row == 0) {
UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
// 获得view controller之后,在这种场景下,到底push还是present,其实是要由使用者决定的,mediator只要给出view controller的实例就好了
[self presentViewController:viewController animated:YES completion:nil];
}
if (indexPath.row == 1) {
UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
[self.navigationController pushViewController:viewController animated:YES];
}
if (indexPath.row == 2) {
// 这种场景下,很明显是需要被present的,所以不必返回实例,mediator直接present了
[[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];
}
if (indexPath.row == 3) {
// 这种场景下,参数有问题,因此需要在流程中做好处理
[[CTMediator sharedInstance] CTMediator_presentImage:nil];
}
if (indexPath.row == 4) {
[[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {
// 做你想做的事
}];
}
if (indexPath.row == 5) {
TableViewController *tableViewController = [[TableViewController alloc] init];
[self presentViewController:tableViewController animated:YES completion:nil];
}
if (indexPath.row == 6) {
[[CTMediator sharedInstance] performTarget:@"InvalidTarget" action:@"InvalidAction" params:nil shouldCacheTarget:NO];
}
}
#pragma mark - getters and setters
- (UITableView *)tableView
{
if (_tableView == nil) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.delegate = self;
_tableView.dataSource = self;
// 注册用于创建新表单元格的类。
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellIdentifier];
}
return _tableView;
}
- (NSArray *)dataSource
{
if (_dataSource == nil) {
_dataSource = @[@"present detail view controller",
@"push detail view controller",
@"present image",
@"present image when error",
@"show alert",
@"table view cell",
@"No Target-Action response"
];
}
return _dataSource;
}
@end
中间件Mediator
CTMediator+CTMediatorModuleAActions.h
//
// CTMediator+CTMediatorModuleAActions.h
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "CTMediator.h"
@interface CTMediator (CTMediatorModuleAActions)
/// 中介 视图详情控制器
- (UIViewController *)CTMediator_viewControllerForDetail;
/**
中介 显示警报信息
@param message 警报信息
@param cancelAction 取消动作回调
@param confirmAction 确定动作回调
*/
- (void)CTMediator_showAlertWithMessage:(NSString *)message cancelAction:(void(^)(NSDictionary *info))cancelAction confirmAction:(void(^)(NSDictionary *info))confirmAction;
/// 中介 模态弹出图片
- (void)CTMediator_presentImage:(UIImage *)image;
/// 中介 返回cell
- (UITableViewCell *)CTMediator_tableViewCellWithIdentifier:(NSString *)identifier tableView:(UITableView *)tableView;
/// 中介 配置cell
- (void)CTMediator_configTableViewCell:(UITableViewCell *)cell withTitle:(NSString *)title atIndexPath:(NSIndexPath *)indexPath;
/// 中介 清除cell点击
- (void)CTMediator_cleanTableViewCellTarget;
@end
//
// CTMediator+CTMediatorModuleAActions.m
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import "CTMediator+CTMediatorModuleAActions.h"
/// 中介目标 A
NSString * const kCTMediatorTargetA = @"A";
/// 中介动作 本地获取详细视图控制器
NSString * const kCTMediatorActionNativeFetchDetailViewController = @"nativeFetchDetailViewController";
/// 中介动作 本地模态弹出图片
NSString * const kCTMediatorActionNativePresentImage = @"nativePresentImage";
/// 中介动作 本地没有图片
NSString * const kCTMediatorActionNativeNoImage = @"nativeNoImage";
/// 中介动作 展示警报
NSString * const kCTMediatorActionShowAlert = @"showAlert";
/// 中介动作 返回cell
NSString * const kCTMediatorActionCell = @"cell";
/// 中介动作 配置cell
NSString * const kCTMediatorActionConfigCell = @"configCell";
@implementation CTMediator (CTMediatorModuleAActions)
/// 中介 视图详情控制器
- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativeFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}
/// 中介 模态弹出图片
- (void)CTMediator_presentImage:(UIImage *)image
{
if (image) {
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativePresentImage
params:@{@"image":image}
shouldCacheTarget:NO];
} else {
// 这里处理image为nil的场景,如何处理取决于产品
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativeNoImage
params:@{@"image":[UIImage imageNamed:@"noImage"]}
shouldCacheTarget:NO];
}
}
/**
中介 显示警报信息
@param message 警报信息
@param cancelAction 取消动作回调
@param confirmAction 确定动作回调
*/
- (void)CTMediator_showAlertWithMessage:(NSString *)message cancelAction:(void(^)(NSDictionary *info))cancelAction confirmAction:(void(^)(NSDictionary *info))confirmAction
{
// 可变参数字典,将要发送出去的参数
NSMutableDictionary *paramsToSend = [[NSMutableDictionary alloc] init];
if (message) {
paramsToSend[@"message"] = message;
}
if (cancelAction) {
paramsToSend[@"cancelAction"] = cancelAction;
}
if (confirmAction) {
paramsToSend[@"confirmAction"] = confirmAction;
}
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionShowAlert
params:paramsToSend
shouldCacheTarget:NO];
}
/// 中介 返回cell
- (UITableViewCell *)CTMediator_tableViewCellWithIdentifier:(NSString *)identifier tableView:(UITableView *)tableView
{
return [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionCell
params:@{
@"identifier":identifier,
@"tableView":tableView
}
shouldCacheTarget:YES];
}
/// 中介 配置cell
- (void)CTMediator_configTableViewCell:(UITableViewCell *)cell withTitle:(NSString *)title atIndexPath:(NSIndexPath *)indexPath
{
// perform Target 执行目标
[self performTarget:kCTMediatorTargetA
action:kCTMediatorActionConfigCell
params:@{
@"cell":cell,
@"title":title,
@"indexPath":indexPath
}
shouldCacheTarget:YES];
}
/// 中介 清除cell点击
- (void)CTMediator_cleanTableViewCellTarget
{
[self releaseCachedTargetWithTargetName:kCTMediatorTargetA];
}
@end
CTMediator
//
// CTMediator.h
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import <UIKit/UIKit.h>
/// 全局常量 中介参数关键字 Swift目标模块名称
extern NSString * const kCTMediatorParamsKeySwiftTargetModuleName;
@interface CTMediator : NSObject
+ (instancetype)sharedInstance;
// 远程App调用入口
/**
远程App调用入口
performActionWithUrl 通过远程URL执行动作
@param url 远程URL
@param completion 动作完成后的回调 字典block参数
@return 返回结果
*/
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地组件调用入口
/**
本地组件调用入口
performTarget 执行目标
@param targetName 目标名称
@param actionName 动作名称
@param params 参数字典
@param shouldCacheTarget 是否缓存目标
@return 返回结果
*/
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
/**
释放缓存的目标 通过目标名称
@param targetName 目标名称
*/
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;
@end
//
// CTMediator.m
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import "CTMediator.h"
#import <objc/runtime.h>
NSString * const kCTMediatorParamsKeySwiftTargetModuleName = @"kCTMediatorParamsKeySwiftTargetModuleName";
@interface CTMediator ()
/// 缓存目标的可变字典
@property (nonatomic, strong) NSMutableDictionary *cachedTarget;
@end
@implementation CTMediator
#pragma mark - public methods
/// 单例
+ (instancetype)sharedInstance
{
static CTMediator *mediator;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mediator = [[CTMediator alloc] init];
});
return mediator;
}
/*
远程App调用入口
核心方法
scheme://[target]/[action]?[params]
url sample:
aaa://targetA/actionB?id=1234
*/
- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
// 查询字符串,符合RFC 1808。
NSString *urlString = [url query];
// componentsSeparatedByString 返回一个包含已被给定分隔符分隔的来自接收器的子字符串的数组。
// 遍历 被 & 分隔的 urlString数组
for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
// 取出被等号分隔的数组
NSArray *elts = [param componentsSeparatedByString:@"="];
// continue 忽略了当次循环continue语句后的代码
if([elts count] < 2) continue;
[params setObject:[elts lastObject] forKey:[elts firstObject]];
}
// 这里这么写主要是出于安全考虑,防止黑客通过远程方式调用本地模块。这里的做法足以应对绝大多数场景,如果要求更加严苛,也可以做更加复杂的安全逻辑。
NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
if ([actionName hasPrefix:@"native"]) {
return @(NO);
}
// 这个demo针对URL的路由处理非常简单,就只是取对应的target名字和method名字,但这已经足以应对绝大部份需求。如果需要拓展,可以在这个方法调用之前加入完整的路由逻辑
id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
// 判断是否有执行完成动作后的回调字典
if (completion) {
// 判断是否有结果
if (result) {
completion(@{@"result":result});
} else {
completion(nil);
}
}
return result;
}
/**
本地组件调用入口
performTarget 执行目标
@param targetName 目标名称
@param actionName 动作名称
@param params 参数字典
@param shouldCacheTarget 是否缓存目标
@return 返回结果
*/
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
// Swift模块名称
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);
// 创建目标类的实例. alloc:分配内存。 init:初始化。
target = [[targetClass alloc] init];
}
//==============================================================
// generate action 生成动作
NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
// NSSelectorFromString 返回带有给定名称的选择器。
// 通过动作字符串 创建动作方法
SEL action = NSSelectorFromString(actionString);
//=============================================================
// 处理 target == nil 的情况.
// 但是上面已经通过 targetClassString = Target_%@ 创建了target对象.
// 所以 这里可能是多余的.
if (target == nil) {
// 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
[self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
return nil;
}
//================================================================
// 判断是否缓存目标
if (shouldCacheTarget) {
// 将目标对象 放入缓存目标的可变字典
self.cachedTarget[targetClassString] = target;
}
//================================================================
// 处理 target 没有 action 的情况
// respondsToSelector 返回一个布尔值,该值指示接收方是否实现或继承可以响应指定消息的方法。
if ([target respondsToSelector:action]) {
// 核心方法 安全的执行action
return [self safePerformAction:action target:target params:params];
} else {
// 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
SEL action = NSSelectorFromString(@"notFound:");
if ([target respondsToSelector:action]) {
// 核心方法 安全的执行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;
}
}
}
/**
释放缓存的目标 通过目标名称
@param targetName 目标名称
*/
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName
{
NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
[self.cachedTarget removeObjectForKey:targetClassString];
}
#pragma mark - private methods 私有方法
/// 没有目标动作响应时 执行的方法
- (void)NoTargetActionResponseWithTargetString:(NSString *)targetString selectorString:(NSString *)selectorString originParams:(NSDictionary *)originParams
{
SEL action = NSSelectorFromString(@"Action_response:");
NSObject *target = [[NSClassFromString(@"Target_NoTargetAction") alloc] init];
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
params[@"originParams"] = originParams;
params[@"targetString"] = targetString;
params[@"selectorString"] = selectorString;
[self safePerformAction:action target:target params:params];
}
/**
确保安全执行动作的方法 通过OC运行时实现的核心方法
@param action 执行动作的方法
@param target 目标对象(执行动作的主体调用者)
@param params 参数字典
@return 返回模块通过Action_方法创建出来的对象
*/
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
// methodSignatureForSelector 返回一个NSMethodSignature对象,该对象包含由给定选择器标识的方法的描述。
NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
if(methodSig == nil) {
return nil;
}
// methodReturnType 在Objective-C类型编码中 对方法的返回类型进行编码的C字符串。
const char* retType = [methodSig methodReturnType];
//=============================================================
/** 注释
1、iOS 的基础数据类型及其包装类型:
1.1、iOS 的基础数据类型
int、float、double、long、char、NSInteger、NSUInteger、CGFloat、BOOL等
基本数据类型——char类型(在计算机内部以int类型存储)
在 64 位平台和类似于 64 位平台的各种平台上,
NSInteger-> long,
NSUInteger-> unsigned long,
CGFloat-> double.
1.2、iOS 的基础数据类型与包装类型的转换
由于只有对象类型才能放入数组、字典中,所以需要将基本数据类型转换成包装类型,OC 中提供的包装类是 NSNumber,NSValue。其中NSNumber 继承于 NSValue。NSNumber 主要针对于基本数据类型的包装,NSValue 主要针对结构体进行包装。
https://www.jianshu.com/p/ff2274430b1c
*/
/** 注释
strcmp函数
是string compare(字符串比较)的缩写,用于比较两个字符串并根据比较结果返回整数。
基本形式为strcmp(str1,str2),
若str1=str2,则返回零;若str1<str2,则返回负数;若str1>str2,则返回正数。
@encode(Type)
@encode()
作用:用来判断类型,常和strcmp(ObjCType, @encode(Type))合用。
@encode(Type) 可以返回该类型的 C 字符串(char *)的表示。
*/
// 下面这些if语句的作用,应该是排除返回值是基本数据类型的情况.
// @encode() 作用:用来判断类型,常和strcmp(ObjCType, @encode(Type))合用。
// 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(void)
if (strcmp(retType, @encode(void)) == 0) {
// 返回一个能够使用给定方法签名构造消息的NSInvocation对象。Invocation 调用
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
// 设置接收者的参数。
/** 注释
atIndex 指数
指定参数索引的整数。
索引0和1分别表示隐藏参数self和_cmd;您应该使用target和selector属性直接设置这些值。对于通常在消息中传递的参数,使用索引2或更大。
*/
[invocation setArgument:¶ms atIndex:2];
// 接收器的选择器,如果没有设置则为0。
[invocation setSelector:action];
// 接收者的目标,如果接收者没有目标,则为nil。
[invocation setTarget:target];
// 将接收方的消息(带有参数)发送到其目标,并设置返回值。
[invocation invoke];
return nil;
}
// 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(NSInteger)
if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSInteger result = 0;
// 获取接收者的返回值。
// 将模块中 方法的返回值得实际值 赋值给result
[invocation getReturnValue:&result];
// 返回oc对象(将返回值的实际值 包装成OC对象)
return @(result);
}
// 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(BOOL)
if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
BOOL result = 0;
[invocation getReturnValue:&result];
return @(result);
}
// 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(CGFloat)
if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
CGFloat result = 0;
[invocation getReturnValue:&result];
return @(result);
}
// 判断 安全执行动作方法(即这个方法) 的返回类型 是否为(NSUInteger)
if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
[invocation setArgument:¶ms atIndex:2];
[invocation setSelector:action];
[invocation setTarget:target];
[invocation invoke];
NSUInteger result = 0;
[invocation getReturnValue:&result];
return @(result);
}
/** 注释
详情链接
https://www.cnblogs.com/lurenq/p/7709731.html
首先#pragma在本质上是声明,常用的功能就是注释,尤其是给Code分段注释;
而且它还有另一个强大的功能是处理编译器警告,但却没有上一个功能用的那么多。
clang diagnostic 是#pragma 第一个常用命令:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-相关命令"
// 你自己的代码
#pragma clang diagnostic pop
*/
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
// 在这里写 自己的代码;
// 这三行 #pragma clang diagnostic 代码,是忽略警告的固定格式.
// 如果你确定不会发生内存泄漏的情况下,可以使用如下的语句来忽略掉这条警告 "-Warc-performSelector-leaks"
// 以对象作为参数 向接收者发送消息。
// 中介的核心代码,目标通过参数执行动作以后,得到Target_A执行后返回的对象.
// 实际上,真正创建出所需要的实例对象的类 是Target_A的类.(Target_A是真正干活的)
id Target_Object = [target performSelector:action withObject:params];
return Target_Object;
#pragma clang diagnostic pop
}
#pragma mark - getters and setters
- (NSMutableDictionary *)cachedTarget
{
if (_cachedTarget == nil) {
_cachedTarget = [[NSMutableDictionary alloc] init];
}
return _cachedTarget;
}
@end
模块部分
Target_A
//
// Target_A.h
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface Target_A : NSObject
/// 动作 本地获取详情视图控制器
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;
/// 动作 本地模态弹出图片
- (id)Action_nativePresentImage:(NSDictionary *)params;
/// 动作 展示警报
- (id)Action_showAlert:(NSDictionary *)params;
// 容错
- (id)Action_nativeNoImage:(NSDictionary *)params;
@end
//
// Target_A.m
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import "Target_A.h"
#import "DemoModuleADetailViewController.h"
typedef void (^CTUrlRouterCallbackBlock)(NSDictionary *info);
@implementation Target_A
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
- (id)Action_nativePresentImage:(NSDictionary *)params
{
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = @"this is image";
// 通过参数字典 取出字典中的对象
viewController.imageView.image = params[@"image"];
// 窗口的根控制器 模态弹出 视图控制器
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
// id 的返回值可以为空
return nil;
}
- (id)Action_showAlert:(NSDictionary *)params
{
// 创建并返回具有指定标题和行为的操作。
// handler : 处理程序
// 当用户选择操作时要执行的块。这个块没有返回值,只接受选择的action对象作为它的唯一参数。
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
// 重点
// 通过参数字典 取出回调的block;参数字典中,包含回调block对象.
CTUrlRouterCallbackBlock callback = params[@"cancelAction"];
// 如果回调有值,执行回调动作.
if (callback) {
callback(@{@"alertAction":action});
}
}];
UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"confirm" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
CTUrlRouterCallbackBlock callback = params[@"confirmAction"];
if (callback) {
callback(@{@"alertAction":action});
}
}];
// 创建 警报控制器
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"alert from Module A" message:params[@"message"] preferredStyle:UIAlertControllerStyleAlert];
// 添加动作
[alertController addAction:cancelAction];
[alertController addAction:confirmAction];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
return nil;
}
/// 动作 本地没有图片(用来容错处理)
- (id)Action_nativeNoImage:(NSDictionary *)params
{
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = @"no image";
viewController.imageView.image = [UIImage imageNamed:@"noImage"];
[[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
return nil;
}
/// 通过动作拿到cell
- (UITableViewCell *)Action_cell:(NSDictionary *)params
{
UITableView *tableView = params[@"tableView"];
NSString *identifier = params[@"identifier"];
// 这里的TableViewCell的类型可以是自定义的,我这边偷懒就不自定义了。
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
return cell;
}
- (id)Action_configCell:(NSDictionary *)params
{
NSString *title = params[@"title"];
NSIndexPath *indexPath = params[@"indexPath"];
UITableViewCell *cell = params[@"cell"];
// 这里的TableViewCell的类型可以是自定义的,我这边偷懒就不自定义了。
cell.textLabel.text = [NSString stringWithFormat:@"%@,%ld", title, (long)indexPath.row];
// if ([cell isKindOfClass:[XXXXXCell class]]) {
// 正常情况下在这里做特定cell的赋值,上面就简单写了
// }
return nil;
}
@end
DemoModuleADetailViewController
//
// DemoModuleADetailViewController.h
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface DemoModuleADetailViewController : UIViewController
/// 只读;外界只能拿到数据 但是不能修改
@property (nonatomic, strong, readonly) UILabel *valueLabel;
@property (nonatomic, strong, readonly) UIImageView *imageView;
@end
//
// DemoModuleADetailViewController.m
// CTMediator
//
// Created by casa on 16/3/13.
// Copyright © 2016年 casa. All rights reserved.
//
#import "DemoModuleADetailViewController.h"
#import <HandyFrame/UIView+LayoutMethods.h>
@interface DemoModuleADetailViewController ()
/// 读写;重写属性,使 点m文件 中的属性 可以被操作
@property (nonatomic, strong, readwrite) UILabel *valueLabel;
@property (nonatomic, strong, readwrite) UIImageView *imageView;
@property (nonatomic, strong) UIButton *returnButton;
@end
@implementation DemoModuleADetailViewController
#pragma mark - life cycle
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor grayColor];
[self.view addSubview:self.valueLabel];
[self.view addSubview:self.imageView];
[self.view addSubview:self.returnButton];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self.valueLabel sizeToFit];
[self.valueLabel topInContainer:70 shouldResize:NO];
// 横坐标居中
[self.valueLabel centerXEqualToView:self.view];
// // 等效于
// self.valueLabel.ct_x = self.view.ct_centerX;
self.imageView.ct_size = CGSizeMake(100, 100);
[self.imageView centerEqualToView:self.view];
self.returnButton.ct_size = CGSizeMake(100, 100);
[self.returnButton bottomInContainer:0 shouldResize:NO];
[self.returnButton centerXEqualToView:self.view];
}
#pragma mark - event response 事件响应
- (void)didTappedReturnButton:(UIButton *)button
{
// 判断当前导航控制器是否为空;从而得知当前控制器弹出的方式
if (self.navigationController == nil) {
// 解散由视图控制器模态呈现的视图控制器。
[self dismissViewControllerAnimated:YES completion:nil];
} else {
[self.navigationController popViewControllerAnimated:YES];
}
}
#pragma mark - getters and setters
- (UILabel *)valueLabel
{
if (_valueLabel == nil) {
_valueLabel = [[UILabel alloc] init];
_valueLabel.font = [UIFont systemFontOfSize:30];
_valueLabel.textColor = [UIColor blackColor];
}
return _valueLabel;
}
- (UIImageView *)imageView
{
if (_imageView == nil) {
_imageView = [[UIImageView alloc] init];
_imageView.contentMode = UIViewContentModeScaleAspectFit;
}
return _imageView;
}
- (UIButton *)returnButton
{
if (_returnButton == nil) {
_returnButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_returnButton addTarget:self action:@selector(didTappedReturnButton:) forControlEvents:UIControlEventTouchUpInside];
[_returnButton setTitle:@"return" forState:UIControlStateNormal];
[_returnButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
}
return _returnButton;
}
@end
主框架调用的TableView二级页面
TableViewController
//
// TableViewController.h
// CTMediator
//
// Created by casa on 2016/10/20.
// Copyright © 2016年 casa. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface TableViewController : UIViewController
@end
//
// TableViewController.m
// CTMediator
//
// Created by casa on 2016/10/20.
// Copyright © 2016年 casa. All rights reserved.
//
#import "TableViewController.h"
#import <HandyFrame/UIView+LayoutMethods.h>
#import "CTMediator+CTMediatorModuleAActions.h"
@interface TableViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) UIButton *closeButton;
@end
@implementation TableViewController
#pragma mark - life cycle
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.tableView];
[self.view addSubview:self.closeButton];
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
[self.tableView fillWidth];
[self.tableView topInContainer:0 shouldResize:YES];
[self.tableView bottomInContainer:50 shouldResize:YES];
[self.closeButton fillWidth];
[self.closeButton top:0 FromView:self.tableView];
[self.closeButton bottomInContainer:0 shouldResize:YES];
}
- (void)dealloc
{
// 在Controller被回收的时候,把相关的target也回收掉
[[CTMediator sharedInstance] CTMediator_cleanTableViewCellTarget];
// [CTMediator.sharedInstance CTMediator_cleanTableViewCellTarget];
}
#pragma mark - UITableViewDelegate
// cell即将显示的时候
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
// 通过Mediator来获取cell实例,由于target已经被cache了,高频调用不是问题。
[[CTMediator sharedInstance] CTMediator_configTableViewCell:cell withTitle:@"cell title" atIndexPath:indexPath];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 100;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 通过mediator来配置cell实例,由于target已经被cache了,高频调用不是问题。
return [[CTMediator sharedInstance] CTMediator_tableViewCellWithIdentifier:@"cell" tableView:tableView];
}
#pragma mark - event response 事件响应
// 点击关闭按钮
- (void)didTappedCloseButton:(UIButton *)button
{
[self dismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - getters and setters
- (UITableView *)tableView
{
if (_tableView == nil) {
_tableView = [[UITableView alloc] init];
_tableView.delegate = self;
_tableView.dataSource = self;
}
return _tableView;
}
- (UIButton *)closeButton
{
if (_closeButton == nil) {
_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_closeButton setTitle:@"Close" forState:UIControlStateNormal];
[_closeButton setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
// 点击按钮要执行的方法
[_closeButton addTarget:self action:@selector(didTappedCloseButton:) forControlEvents:UIControlEventTouchUpInside];
_closeButton.backgroundColor = [UIColor grayColor];
}
return _closeButton;
}
@end
Demo链接
https://github.com/casatwy/CTMediator
引用
组件化架构漫谈 https://www.jianshu.com/p/67a6004f6930
iOS应用架构谈 组件化方案 https://casatwy.com/iOS-Modulization.html
iOS中的三种路由方案实践 https://www.jianshu.com/p/72d705ecc177
iOS 组件化 —— 路由设计思路分析 https://www.jianshu.com/p/76da56b3bd55