iOS高级开发技术iOS开发记录iOS程序猿

一步步撸一个app模块化路由、URLscheme访问

2018-10-27  本文已影响79人  _maomao
为什么要做路由
这个问题就要提到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"]];

注册一个URL Scheme

屏幕快照 2018-10-27 16.21.24.png

这样其他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" 

GitHub https://github.com/wangyin1/YINRoute

上一篇下一篇

猜你喜欢

热点阅读