为什么要做路由
这个问题就要提到app 开发模块化的思想了,试想一下你的app是一个电商项目,那么你的产品详情页、列表页、购物车、搜索等页面肯定就是调用频次非常高的VC了,这些界面之间跳转都会非常频繁。这就造成了互相依赖并且高度耦合。如下图: image是不是非常的乱?相互之间都有依赖,在列表需要import详情页面,详情页面也要import列表,购物车等等。你有可能说,这没什么啊,丝毫不影响开发效率。但是当一个app在公司业务发展的过程中体积越来越庞大,其中堆叠了大量的业务逻辑代码,不同业务的代码相互调用,相互嵌套,代码之间的耦合性越来越高,调用逻辑会越来越混乱,代码看来起就很糟糕了,将来的升级就需要对代码进行大量修改及调整,带来的工作量是非常巨大的。一个良好的开始,非常的重要。模块之间要相互独立,如果你的代码vc之间的import很多,那就说明耦合度很高了。模块化可以将代码的功能逻辑尽量封装在一起,对外只提供接口,业务逻辑代码与功能模块通过接口进行弱耦合。这就需要设计一套符合要求的组件之间通信的中间件,这个中间件就称为路由。
image
下面我们开始撸代码
app内部访问
创建route管理类
根据上图很简单的就想到了一个方法,提供一个中间层:Router。在router里面定义好每次跳转的方法,然后在需要用的界面调用router函数,传入对应的参数。比如这样:
#import "GoodsDetailViewController.h"
#import "ShopCartViewController.h"
@implementation Router
+ (UIViewController *) getDetailWithParam:(NSString *) param {
GoodsDetailViewController *detailVC = [[GoodsDetailViewController alloc] initWithProId:self.proId];
return detailVC;
}
+ (UIViewController *) getCart {
ShopCartViewController *cartVC = [[ShopCartViewController alloc] init];
return cartVC;
}
@end
在其他页面这样使用
#import "Router.m"
UIViewController * detailVC = [[Router instance] getDetailWithParam:param];
[self.navigationController pushViewController: detailVC];
但是这样做也有一个问题,Router里面会依赖所有的VC。那如何打破这层循环引用呢?
利用OC runtime的特性 动态初始化对象 打破import魔咒
代码
UIViewController * _Nonnull y_controller(NSString *name){
if (!name||name.length==0) {
LogError(@"请传入class名");
return nil;
}
id vc = [[NSClassFromString(name) alloc] init];
if (vc) {
if ([vc isKindOfClass:[UIViewController class]]) {
return vc;
}
NSString *error = [NSString stringWithFormat:@"Class %@不是controller",name];
LogError(error);
return nil;
}else{
NSString *error = [NSString stringWithFormat:@"Class %@不存在",name];
LogError(error);
return nil;
}
}
这样我们就能动态获取到controller了,那么如果传递参数呢,每个模块定制的对外接口参数肯定不一样,同样的我们可以用kvc的形式进行动态参数传递
//调用某个页面
- (BOOL)pushVcName:(NSString *)vcName from:(UIViewController *)fromvc withData:(NSDictionary *)data{
UIViewController *vc = y_controller(vcName);
return [self push:vc from:fromvc withData:data];
}
- (BOOL)push:(UIViewController *)vc from:(UIViewController *)fromvc withData:(NSDictionary *)data;{
UIViewController *a = fromvc?fromvc:y_currentController();
if (!vc) {
return NO;
}
//根据字典进行属性赋值
[data enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
@try {
[vc setValue:obj forKey:key];
} @catch (NSException *exception) {
} @finally {
}
}];
[a.navigationController pushViewController:vc animated:YES];
return YES;
}
进行到这里,你可能想问,我们回调怎么处理,vc业务之间肯定少不了回调。
这里我想到的一种解决办法是,同样的可以把block放到data字典参数里面:
id callback = ^(NSString *pass){
NSLog(@"%@", pass);
};
//callback 目标页面有这个block属性即可
@{"name":@"sdsaad",@"callBack":callback}
外部调用示例
id call = ^(NSString *aa){
NSLog(@"%@",aa);
};
[[YINRouteManager shareInstance] pushVcName:@"LoginViewController" from:self withData:@{@"callBack":call,@"name":@"test"}];
如果觉得模块之间用类名进行访问太复杂了(因为oc是无命名空间的,类名通常都比较的长,而且用类名进行通信比较的敏感),或者是在项目开发中,对应的模块还没开发出来,我们要进行访问。怎么办呢?我们可以先定义模块标识。比如
//login是定义的登陆模块 对应LoginViewController
[[YINRouteManager shareInstance] pushVcName:@"login" from:self withData:@{@"callBack":call,@"name":@"test"}];
模块注册路由标识
@implementation LoginViewController
+ (void)load{
[self y_registPath:@"login"];
}
@implementation UIViewController (YINRoute)
+ (void)y_registPath:(NSString *)appOpenPath{
if (!appOpenPath||appOpenPath.length<1) {
[[YINRouteURLPathRegist shareInstance] removePathRegist:NSStringFromClass(self.class)];
}else{
[[YINRouteURLPathRegist shareInstance] registClass:NSStringFromClass(self.class) withPath:appOpenPath];
}
}
@implementation YINRouteURLPathRegist
- (void)registClass:(NSString *)className withPath:(NSString *)path;{
if (!className||className.length==0) {
return;
}
if (!path||path.length==0) {
[self removePathRegist:className];
return;
}
[self.registDict setObject:className forKey:path];
}
- (void)removePathRegist:(NSString *)className;{
if (!className||className.length==0) {
return;
}
__block NSString *removeKey = nil;
[self.registDict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if ([obj isEqualToString:className]) {
removeKey = key;
*stop = YES;
}
}];
if (removeKey) {
[self.registDict removeObjectForKey:removeKey];
}
}
标示注册记录好之后,在获取controller的方法里面要作一点调整
UIViewController * _Nonnull y_controller(NSString *name){
if (!name||name.length==0) {
LogError(@"请传入class名");
return nil;
}
//如果此路由标示有对应的类名 注册
if ([[YINRouteURLPathRegist shareInstance].registDict objectForKey:name]) {
//取出真实的类名
name = [[YINRouteURLPathRegist shareInstance].registDict objectForKey:name];
}
id vc = [[NSClassFromString(name) alloc] init];
if (vc) {
if ([vc isKindOfClass:[UIViewController class]]) {
return vc;
}
NSString *error = [NSString stringWithFormat:@"Class %@不是controller",name];
LogError(error);
return nil;
}else{
NSString *error = [NSString stringWithFormat:@"Class %@不存在",name];
LogError(error);
return nil;
}
}
每次使用route进行访问太麻烦,创建一个controller的分类
@implementation UIViewController (YINRoute)
+ (void)y_registPath:(NSString *)appOpenPath{
if (!appOpenPath||appOpenPath.length<1) {
[[YINRouteURLPathRegist shareInstance] removePathRegist:NSStringFromClass(self.class)];
}else{
[[YINRouteURLPathRegist shareInstance] registClass:NSStringFromClass(self.class) withPath:appOpenPath];
}
}
- (BOOL)y_push:(UIViewController *)vc;{
return [self y_push:vc withData:nil];
}
- (BOOL)y_present:(UIViewController *)vc;{
return [self y_present:vc withData:nil];
}
- (BOOL)y_push:(UIViewController *)vc withData:(NSDictionary *)data;{
return [[YINRouteManager shareInstance] push:vc from:self withData:data];
}
- (BOOL)y_present:(UIViewController *)vc withData:(NSDictionary *)data;{
return [[YINRouteManager shareInstance] present:vc from:self withData:data];
}
- (BOOL)y_pushVcName:(NSString *)vcName;{
return [self y_pushVcName:vcName withData:nil];
}
- (BOOL)y_presentVcName:(NSString *)vcName;{
return [self y_presentVcName:vcName withData:nil];
}
- (BOOL)y_pushVcName:(NSString *)vcName withData:(NSDictionary *)data;{
return [[YINRouteManager shareInstance] pushVcName:vcName from:self withData:data];
}
- (BOOL)y_presentVcName:(NSString *)vcName withData:(NSDictionary *)data;{
return [[YINRouteManager shareInstance] presentVcName:vcName from:self withData:data];
}
这样一个简单的app内部路由器就成型了。
App之间的访问 撸一个外部路由
那么app之间的跳转有什么作用呢?我们所使用的每一个app就相当于一个功能,也是一个模块。app的跳转可以使得每个app就像一个功能组件一样,帮助我们完成需要做的事情,比如三方支付,搜索,导航,分享等等。
iOS提供了URL Scheme的解决方案
要跳转到别人的app,就要知道别人的app的跳转协议是什么,需要传入什么参数,我们常见的跳转协议有下面这些:
1.打开Mail
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"mailto://info@icloud.com"]]
2.打开电话
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"tel://18688886666"]];
3.打开SMS
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"sms:18688886666"]];
屏幕快照 2018-10-27 16.21.24.png注册一个URL Scheme
这样其他app就可以通过此urlschemes来访问了
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"BLBaseAPP://"]];
制定协议
app间的访问有两种情况1.打开某个页面2.执行某个函数。其实也只可以理解为执行某个函数。两种情况只是细分
建议:
在host位传入通讯类型 (page或action) path位如果是page类型就传入页面标识 action类型就传入方法标识
//NSURL *url = [NSURL URLWithString:@"urlscheme://host/path?page=100"];
创建YINRouteURLData用于接受处理URL
typedef NS_ENUM(NSInteger, YINAppRouteType) {
YINAppRouteTypePage = 0, // 打开页面
YINAppRouteTypeAction = 1, // 调用方法
};
@implementation YINRouteURLData
+ (instancetype)urlDataWithUrl:(NSURL *)url;{
YINRouteURLData *a = [[self alloc] init];
a.URL = url;
return a;
}
- (NSURL *)URL{
if (!_URL) {
_URL = [NSURL URLWithString:@"app://"];
}
return _URL;
}
//如果是页面访问类型,获取页面标示
- (NSString *)controllerName{
if (self.routeType == YINAppRouteTypePage) {
NSString *path =self.URL.path.length>0?[self.URL.path substringFromIndex:1]:@"";
return path;
}
return nil;
}
/**
路由类型 YINAppRouteTypePage = 0, // 打开页面
YINAppRouteTypeAction = 1, // 调用方法
*/
- (YINAppRouteType)routeType{
if (self.URL.host&&[self.URL.host isEqualToString:[YINRouteConfig actionHost]]) {
return YINAppRouteTypeAction;
}
return YINAppRouteTypePage;
}
//执行的方法标示
- (NSString *)actionName{
if (self.routeType == YINAppRouteTypeAction) {
return self.URL.path.length>0?[self.URL.path substringFromIndex:1]:@"";
}
return nil;
}
//url参数解析为字典
- (NSDictionary *)data{
NSString *dataStr = [NSString stringWithFormat:@"%@",self.URL.query];
NSArray *keyValues = [dataStr componentsSeparatedByString:@"&"];
NSMutableDictionary *dic = @{}.mutableCopy;
[keyValues enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[dic setObject:[obj componentsSeparatedByString:@"="].lastObject forKey:[obj componentsSeparatedByString:@"="].firstObject];
}];
return dic;
}
appdelegate接受到url访问
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{
[[YINRouteManager shareInstance] appActionWithUrl:url];
return YES;
}
YINRouteManager内部处理
//处理app间的通信、跳转等事件
- (BOOL)appActionWithUrl:(NSURL *)url{
if (![[YINRouteConfig urlScheme] containsObject:url.scheme]) {
return NO;
}
YINRouteURLData *data = [YINRouteURLData urlDataWithUrl:url];
if (data.routeType==YINAppRouteTypePage) {
//页面跳转类型
return [self pushVcName:data.controllerName from:nil withData:data.data];
}else{
#warning 请在此处根据需求实现逻辑
//方法执行
// NSString *actionName = data.actionName;
// NSString *actionData = data.data;
}
return YES;
}
url访问控制的配置
@interface YINRouteConfig : NSObject
//判断通讯类型为方法执行
@property (copy,nonatomic) NSString *actionHost;
//判断通讯类型为页面跳转
@property (strong,nonatomic) NSString *openPageHost;
//url通信时对满足此条件的urlScheme进行路由控制 为什么使用数组呢 因为有可能对不同的介入app开放不同的urlscheme 一般情况只判断一种
@property (copy,nonatomic) NSArray <NSString *> * urlScheme;
+ (instancetype)shareInstance;
@end
开启URL访问功能,并配置规则
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[YINRouteManager startWithUrlSchemes:@[@"YINRouteDemo"] pageHost:@"open" actionHost:@"action" actionBlock:^(NSString *actionName, id data) {
NSLog(@"执行方法%@",actionName);
NSLog(@"参数%@",data);
}];
return YES;
}
这样我们就能通过URL的形式访问app所有的页面和执行一些特定方法
//访问页面
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"YINRouteDemo://open/LoginViewController?name=123213&pass=123"]];
//执行方法
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"YINRouteDemo://action/logPrint?name=123213&pass=123"]];
内部路由和外部路由规则统一
通过上面的代码,已经做到了规则统一。这样我们也可以用这样的代码访问app内的任何模块了
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"YINRouteDemo://open/LoginViewController?name=123213&pass=123"]];
如果在浏览器或者其他app访问这个链接,则会打开app然后进入登陆页面并传递参数。
当然我们如果给LoginViewController注册了路由标示为login
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"YINRouteDemo://open/login?name=123213&pass=123"]];
也是一样的效果
如果是在app内访问,直接跳转到登陆页面。
app内建议还是用controller的分类进行路由就好了。
总结
YINRoute
app模块化路由管理器,app间urlscheme访问管理器
调用示例
id call = ^(NSString *aa){
NSLog(@"%@",aa);
};
[[YINRouteManager shareInstance] pushVcName:@"LoginViewController" from:self withData:@{@"callBack":call,@"name":@"test"}];
设置特殊路由标示
@implementation LoginViewController
+ (void)load{
//设置了路由标示后 既可以通过类名访问 也可以通过标示访问
[self y_registPath:@"login"];
}
controller分类方法快捷调用
//页面跳转
[self y_pushVcName:@"LoginViewController" withData:@{
@"name":@"12323121"
}];
[self y_pushVcName:@"login" withData:@{
@"name":@"12323121"
}];
url形式访问模块 此方法同时支持app 内模块间访问 也支持app之间的访问
开启url访问功能
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[YINRouteManager startWithUrlSchemes:@[@"BLBaseAPP"] pageHost:@"open" actionHost:@"action" actionBlock:^(NSString *actionName, id data) {
NSLog(@"执行方法%@",actionName);
NSLog(@"参数%@",data);
}];
return YES;
}
访问页面
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"BLBaseAPP://open/LoginViewController?name=123213&pass=123"]];
执行方法
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"BLBaseAPP://action/logPrint?name=123213&pass=123"]];
集成
pod "YINRoute"
网友评论