美文网首页iOS Collectionios技术重塑
iOS使用自定义URL实现控制器之间的跳转

iOS使用自定义URL实现控制器之间的跳转

作者: Dariel | 来源:发表于2016-08-19 15:01 被阅读10687次

    一个app往往有很多界面,而界面之间的跳转也就是对应控制器的跳转,控制器的跳转一般有两种情况 push 或者 modal,push 和 modal 的默认效果是系统提供的,但也可以自定义.有兴趣了解一下自定义的童鞋可以看这篇,iOS动画指南 - 6.可以很酷的转场动画.

    文章配图

    1. 概述

    系统提供的push和modal方法有时并不能满足实际需求.比如,我们需要根据服务器返回的字段跳到指定的控制器,难道作判断吗?那显然不是最佳解决方案.

    其实我们可以这样:

        NSString *urlStr = @"dariel://twoitem?name=dariel&userid=213213";  
        // push
        [DCURLRouter pushURLString:urlStr animated:YES];  
        // modal
        [DCURLRouter presentURLString:urlStr animated:YES completion:nil];
    

    对的,就是通过自定义URL+拼接参数,实现跳转.当然啦,DCURLRouter的功能远不止这点.

    2.DCURLRouter的基本使用

    DCURLRouter是一个通过简单配置就能够实现自定义URL跳转的开源组件: GitHub
    你的star是对我最好的支持.😃

    1.简单集成

    只要把DCURLRouter这个文件夹拖到项目中就行了,或者也可以使用cocoapods.

    2. 简单配置

    1. 每一个自定义的URL都会有一个对应的控制器,那Xocde怎么知道呢?我们需要一个plist文件.打开DCURLRouter.plist文件

      内部结构大概长这样.除了自定义的URL上面还有httphttps,这是当如果URL是网页链接的时候,DCURLRouter会自动跳转到自定义好的webView控制器,并把URL当成参数传递到webView控制器.是不是很方便. 下面的dariel字典就是用来存放自定义URL以及对应的控制器名称的.dariel就是自定义协议头了.以后就可以把自定义的URL和对应的控制器放这里了.
    2. 加载DCURLRouter.plist文件数据
    - (BOOL)application:(UIApplication *)application  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
           [DCURLRouter loadConfigDictFromPlist:@"DCURLRouter.plist"];
           return YES;
    }
    

    3. push和modal的使用

    所有的push和modal方法都可以通过DCURLRouter这个类方法来调用.这样在push和modal的时候就不需要拿到导航控制器或控制器再跳转了.也就是说,以后push和modal控制器跳转就不一定要在控制器中进行了.

    1. push控制器
        // 不需要拼接参数直接跳转
        [DCURLRouter pushURLString:@"dariel://twoitem" animated:YES];
        
        // 直接把参数拼接在自定义url末尾
        NSString *urlStr = @"dariel://twoitem?name=dariel&userid=213213";
        [DCURLRouter pushURLString:urlStr animated:YES];
        // 可以将参数放入一个字典
        NSDictionary *dict = @{@"userName":@"Hello", @"userid":@"32342"};
        [DCURLRouter pushURLString:@"dariel://twoitem" query:dict animated:YES];
    
        // 如果当前控制器和要push的控制器是同一个,可以将replace设置为Yes,进行替换.
        [DCURLRouter pushURLString:@"dariel://oneitem" query:dict animated:YES replace:YES];
        
        // 重写了系统的push方法,直接通过控制器跳转
        TwoViewController *two = [[TwoViewController alloc] init];
        [DCURLRouter pushViewController:two animated:YES];
    
    1. modal控制器
      用法和push差不多,只是这里添加了一个给modal出来的控制器加一个导航控制器的方法.
       // 不需要拼接参数直接跳转
       [DCURLRouter presentURLString:@"dariel://threeitem" animated:YES completion:nil];
       
       // 直接把参数拼接在自定义url末尾
       NSString *urlStr = @"dariel://threeitem?name=dariel&userid=213213";
       [DCURLRouter presentURLString:urlStr animated:YES completion:nil];
    
       // 可以将参数放入一个字典
       NSDictionary *dict = @{@"userName":@"Hello", @"userid":@"32342"};
       [DCURLRouter presentURLString:@"dariel://threeitem" query:dict animated:YES completion:nil];
    
       // 给modal出来的控制器添加一个导航控制器
       [DCURLRouter presentURLString:@"dariel://threeitem" animated:YES withNavigationClass:[UINavigationController class] completion:nil];
    
       // 重写了系统的push方法
       ThreeViewController *three = [[ThreeViewController alloc] init];
       [DCURLRouter presentViewController:three animated:YES completion:nil];
    

    4. 后退 pop 和 dismiss

    在实际开发中,好几次的界面的跳转组成了一个业务流程,整个业务流程结束后通常会要求返回最开始的界面,这就要让控制器连续后退好几次,但苹果是没有提供方法的.DCURLRouter给出了具体的实现方案.
    pop:

       /** pop掉一层控制器 */
       + (void)popViewControllerAnimated:(BOOL)animated;
       /** pop掉两层控制器 */
       + (void)popTwiceViewControllerAnimated:(BOOL)animated;
       /** pop掉times层控制器 */
       + (void)popViewControllerWithTimes:(NSUInteger)times animated:(BOOL)animated;
       /** pop到根层控制器 */
       + (void)popToRootViewControllerAnimated:(BOOL)animated;
    

    dismiss:

        /** dismiss掉1层控制器 */
        + (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion;
        /** dismiss掉2层控制器 */
        + (void)dismissTwiceViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion;
        /** dismiss掉times层控制器 */
        + (void)dismissViewControllerWithTimes:(NSUInteger)times animated: (BOOL)flag completion: (void (^ __nullable)(void))completion;
        /** dismiss到根层控制器 */
        + (void)dismissToRootViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion;
    

    5.参数的接收,以及其它方法

    在3中如果在自定义了URL后面拼接了参数,或者用字典传递了参数,那么在目的控制器怎么接收呢?其实参数的接收很简单.只要导入这个分类#import "UIViewController+DCURLRouter.h"就行了,然后就能拿到这三个参数.

        NSLog(@"接收的参数%@", self.params);
        NSLog(@"拿到URL:%@", self.originUrl);
        NSLog(@"URL路径:%@", self.path);
    

    但有时我们我需要把值传递给发送push或者modal方的控制器,也就是逆传,也很简单,可以用代理或者block.有方法可以拿到当前的控制器,以及导航控制器

      
        // 拿到当前控制器
        UIViewController *currentController = [DCURLRouter sharedDCURLRouter].currentViewController;
        // 拿到当前控制器的导航控制器
        UINavigationController *currentNavgationController = [DCURLRouter sharedDCURLRouter].currentNavigationViewController;
    
    

    至此怎么使用就说完了,不知道感觉怎样呢?

    3.DCURLRouter自定义URL跳转的的实现原理.

    1.文件结构

    首先看一下几个文件分别是干什么用的?


    • DCURLRouter是个单例,是主要类,所有对外的接口都是由它提供.我们就是用它通过调用类方法来实现自定义URL跳转的.
    • DCURLNavgation也是单例,主要是用来重写和自定义系统的跳转方法.
    • UIViewController+DCURLRouter 是UIViewController的分类,用于接收控制器的参数,以及用来创建控制器的.
    • DCSingleton 单例的宏 只要在需要创建单例的类中分别导入.h文件中DCSingletonH(类名) .m文件中DCSingletonM(类名) ,这样就可以很方便的创建单例了.具体看代码.
    • DCURLRouter.plist 就是用来存放与自定义URL对应的控制器名称的.

    2.一个自定义URL字符串的push原理

    1. 跳转前我们需要为自定义的URL,设置一个对应的控制器.然后在对应的控制器中执行push操作,就能够push到对应的控制器了.


        [DCURLRouter pushURLString:@"dariel://threeitem" animated:YES];
    
    1. 执行完上面一句代码,经过一些简单处理,最后会来到这里.#import "UIViewController+DCURLRouter.h"的这个方法中
    + (UIViewController *)initFromURL:(NSURL *)url withQuery:(NSDictionary *)query fromConfig:(NSDictionary *)configDict
    {
       UIViewController *VC = nil;
       NSString *home;
       if(url.path == nil){ // 处理url,去掉有可能会拼接的参数
           home = [NSString stringWithFormat:@"%@://%@", url.scheme, url.host];
       }else{
           home = [NSString stringWithFormat:@"%@://%@%@", url.scheme, url.host,url.path];
       }
       if([configDict.allKeys containsObject:url.scheme]){ // 字典中的所有的key是否包含传入的协议头
           id config = [configDict objectForKey:url.scheme]; // 根据协议头取出值
           Class class = nil;
           if([config isKindOfClass:[NSString class]]){ //当协议头是http https的情况
               class =  NSClassFromString(config);
           }else if([config isKindOfClass:[NSDictionary class]]){ // 自定义的url情况
               NSDictionary *dict = (NSDictionary *)config;
               if([dict.allKeys containsObject:home]){
                   class =  NSClassFromString([dict objectForKey:home]); // 根据key拿到对应的控制器名称
               }
           }
           if(class !=nil){
               VC = [[class alloc]init];
               if([VC respondsToSelector:@selector(open:withQuery:)]){
                   [VC open:url withQuery:query];
               }
           }
           // 处理网络地址的情况
           if ([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) {
               class =  NSClassFromString([configDict objectForKey:url.scheme]);
               VC.params = @{@"urlStr": [url absoluteString]};
           }
       }
       return VC;
    }
    

    在这个方法中将自定义URL创建成对应的控制器.具体啥的写的很明白了,就不详细说了啊!

    1. 传参的接收
      注意到上面的[VC open:url withQuery:query];吗?是在下面这个方法中完成赋值的,但我们都有个常识,怎么在分类中保存属性呢?
    - (void)open:(NSURL *)url withQuery:(NSDictionary *)query{
       self.path = [url path];
       self.originUrl = url;
       if (query) {   // 如果自定义url后面有拼接参数,而且又通过query传入了参数,那么优先query传入了参数
           self.params = query;
       }else {
           self.params = [self paramsURL:url];
       }
    }
    

    答案是利用runtime,runtime可以为我们做好这个.

    - (void)setOriginUrl:(NSURL *)originUrl {
        // 为分类设置属性值
       objc_setAssociatedObject(self, &URLoriginUrl,
                                originUrl,
                                OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    - (NSURL *)originUrl {
       // 获取分类的属性值
       return objc_getAssociatedObject(self, &URLoriginUrl);
    }
    
    1. DCURLRouter方法中我们可以拿到在2中返回的VC,然后我们需要到DCURLNavgation中调用push方法了
    + (void)pushURLString:(NSString *)urlString animated:(BOOL)animated {
       UIViewController *viewController = [UIViewController initFromString:urlString fromConfig:[DCURLRouter sharedDCURLRouter].configDict];
       [DCURLNavgation pushViewController:viewController animated:animated replace:NO];
    }
    
    1. DCURLNavgation中怎样去处理push
     + (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated replace:(BOOL)replace
     {
           if (!viewController) {
            NSAssert(0, @"请添加与url相匹配的控制器到plist文件中,或者协议头可能写错了!");
       }
       else {
           if([viewController isKindOfClass:[UINavigationController class]]) {
               [DCURLNavgation setRootViewController:viewController];
           } // 如果是导航控制器直接设置为根控制器
           else {
               UINavigationController *navigationController = [DCURLNavgation sharedDCURLNavgation].currentNavigationViewController;
               if (navigationController) { // 导航控制器存在
                   // In case it should replace, look for the last UIViewController on the UINavigationController, if it's of the same class, replace it with a new one.
                   if (replace && [navigationController.viewControllers.lastObject isKindOfClass:[viewController class]]) {
                                           
                       NSArray *viewControllers = [navigationController.viewControllers subarrayWithRange:NSMakeRange(0, navigationController.viewControllers.count-1)];
                       [navigationController setViewControllers:[viewControllers arrayByAddingObject:viewController] animated:animated];
                   } // 切换当前导航控制器 需要把原来的子控制器都取出来重新添加
                   else {
                       [navigationController pushViewController:viewController animated:animated];
                   } // 进行push
               }
               else {
                   navigationController = [[UINavigationController alloc]initWithRootViewController:viewController];
                   [DCURLNavgation sharedDCURLNavgation].applicationDelegate.window.rootViewController = navigationController;
               } // 如果导航控制器不存在,就会创建一个新的,设置为根控制器
           }
       }
     }
    

    代码写的很详细,就不详细说了啊!

    1. 大概同理,DCURLNavgation中怎样去处理modal
     + (void)presentViewController:(UIViewController *)viewController animated: (BOOL)flag completion:(void (^ __nullable)(void))completion
    {
        if (!viewController) {
             NSAssert(0, @"请添加与url相匹配的控制器到plist文件中,或者协议头可能写错了!");
        }else {
            UIViewController *currentViewController = [[DCURLNavgation sharedDCURLNavgation] currentViewController];
            if (currentViewController) { // 当前控制器存在
                [currentViewController presentViewController:viewController animated:flag completion:completion];
            } else { // 将控制器设置为根控制器
                [DCURLNavgation sharedDCURLNavgation].applicationDelegate.window.rootViewController = viewController;
            }
        }
    }
    

    代码也很详细,有问题可以在下面留言!

    4. 怎样去加载一个自定义的webView控制器

    在上面3.2.2中,不知道有没有注意到那个对网络地址的处理

    // 处理网络地址的情况 
    if ([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) { 
    class = NSClassFromString([configDict objectForKey:url.scheme]); 
    VC.params = @{@"urlStr": [url absoluteString]};
    

    如果协议头是http或者https的情况,我们可以通过[configDict objectForKey:url.scheme]拿到自定义webView控制器的名称,然后再去创建webView控制器,之后我们是将url通过参数传到webView控制器中,最后在webView控制器中加载对应的webview.

    5.关于怎样一次性pop和dismiss多层控制器的实现原理.

    1. pop控制器
    + (void)popViewControllerWithTimes:(NSUInteger)times animated:(BOOL)animated {
          UIViewController *currentViewController = [[DCURLNavgation sharedDCURLNavgation] currentViewController];
       NSUInteger count = currentViewController.navigationController.viewControllers.count;
       if(currentViewController){
           if(currentViewController.navigationController) {
               if (count > times){
                   [currentViewController.navigationController popToViewController:[currentViewController.navigationController.viewControllers objectAtIndex:count-1-times] animated:animated];
               }else { // 如果times大于控制器的数量
                   NSAssert(0, @"确定可以pop掉那么多控制器?");
               }
           }
       }
    }
    

    popViewController实现的思路比较简单,因为可以拿到导航控制器上的所有控制器,然后通过objectAtIndex这个方法.这样就能做到了.

    1. dismiss控制器
    + (void)dismissViewControllerWithTimes:(NSUInteger)times animated: (BOOL)flag completion: (void (^ __nullable)(void))completion {
       UIViewController *rootVC = [[DCURLNavgation sharedDCURLNavgation] currentViewController];
       
       if (rootVC) {
           while (times > 0) {
               rootVC = rootVC.presentingViewController;
               times -= 1;
           }
           [rootVC dismissViewControllerAnimated:YES completion:completion];
       }
       if (!rootVC.presentedViewController) {
           NSAssert(0, @"确定能dismiss掉这么多控制器?");
       }
    }
    

    dismissViewController这个的实现思路就有点特别了,因为没有办法拿到所有的modal出来的控制器,只能拿到上一个,所以这边就是用的while循环实现的.

    5.总结

    大概讲了下具体的使用和大概功能的实现,还有很多具体实现细节,有兴趣的童鞋可以看给出的源码!
    DCURLRouter组件源码: https://github.com/DarielChen/DCURLRouter
    欢迎使用,欢迎star,你的star就是对我最好的鼓励.

    相关文章

      网友评论

      • Sunyc2016:xib和 storyboard需要怎么处理呢?
      • Jesse_5461:回调怎么实现呢? 虽然在第一个VC中能拿到第二个VC,但是都是滞后的, 实现不了第二个VC的block的呢, 要提前在第一个vc中设置第二个vc的属性吗?
        Dariel:你在push下面拿到当前控制器也就是第二个,然后设置block啊
      • 林_柏显:请问如果使用[DCURLRouter pushURLString:[NSString stringWithFormat:@"dariel://shopVCitem?listModel=%@",listModel.slug] animated:YES];这种方式如果带了StoryBoard的控制器该怎么跳转呢
      • 须臾以北____:使用url的方式传参,很明显要把所有参数都转化成字符串拼接到url里,简单来说就是json型参数,如果需要传一个UIImage呢?其次,有需求要对URL加入sign校验,符合签名校验规则才跳.
        Dariel:@须臾以北____ 嗯 其实DCURLRouter主要是用来解决当服务器给了这样的URL(dariel://twoitem?name=dariel&userid=213213)后怎么去做的问题
        须臾以北____:@Dariel在杭州 还是#import了
        Dariel:@须臾以北____ 对UIImage 可以自己添加属性这样做 TwoViewController *two = [[TwoViewController alloc] init]; [DCURLRouter pushViewController:two animated:YES] 校验应该是你自己先处理好,再去决定跳不跳吧
      • Courage_SC:push 到下一页 如何隐藏底部tabbar,有whenhidetabbar的属性吗
      • slimsallen:为什么pop不了 我的count 和times 都是1 要怎么设置呢
        slimsallen:@Dariel在杭州 找到原因了 因为我的跟视图 没有设置导航 所以 count 和times 永远是1
        Dariel:@音乐君 控制台输出啥了
      • ee84a2cd97ab:很明显你没有考虑到SB和Xib
      • 大牛在郑州: [[DCURLRouter sharedDCURLRouter].currentViewController 是能拿到VC,拿到后,我要反向传值怎么传呢?文章说代理或者block具体能给个例子吗?
        Dariel:@NiuNaruto 使用代理或者block啊,oneVC里面不是可以拿到twoVc吗?
        大牛在郑州:@Dariel在杭州在dome里,twoVC里做了一些数据的改变,然后要改变oneVC里的数据,这样怎么实现呢?
        Dariel:@NiuNaruto 当你在那个控制器里执行完push操作后currentViewController就是目标控制器了
      • 风御轩:可以借鉴下LZ的方法,现在的需求正需要这个
      • 菜先生:demo一运行就奔溃...
        Dariel:@菜先生 啥报错啊
      • 知傲:又一个轮子
        Dariel:@zhao0 业余造轮子:joy:
      • songgp:我在要push的控制器的.h文件里写了block,怎么使用你这个方法调用?也就是逆传?
        Jesse_5461:@Dariel在杭州 但是编译前拿不到控制器的属性啊,怎么实现回调呢
        songgp:好的,谢谢
        Dariel:@songgp 在pushURLString方法下面调用currentViewController拿到要push的控制器
      • voidxin:OC和Swift混编时,swift的controller返回为nil,作者可否完善一下,使其兼容swift。
        Dariel:@voidxin 已采纳:smile: 为空的原因是,在swift中字符串转类名需要加个命名空间
      • cavalier_z:很多人觉得这东西没用,其实不是这样的,这东西做了一个非常有意思的事情,就是根据Url来确定Controller。用处有以下几点
        1.假设某个Controller上有一个图标,点击这个图标具体跳哪个页面并不能写死,比如有时候有活动了,就跳一个活动的H5页面,有时候呢又需要跳到某个原生的商品列表页。那么后端只需要把这个url传给客户端,客户端根据这个url跳就可以了,完全不需要管跳到哪个页面。(Android也是有一套一样的逻辑)
        2.当公司大了就会有很多组,那么A组直接调用B组的Controller的话,耦合性太强了。。所以A组会用url跳转。
        3.组多了以后,那么url和controller的关系就应该由每个组来定,所以这套直接写到plist里的方案,会导致每个组都要去改这个plist...挺麻烦的,所以必须得想一套能够让各个组自己定义关系的方案
        风御轩:层主的第一点正是目前的需求,正在搜寻最佳的解决方案。。。
        Dariel:@castlecavalier 是的,有没有用关键还是要看需求。plist文件其实可以有多个,只要做下拼接就可以了,后续会做处理:smile:
      • Rchongg:最近看了几篇URLRouter的文章 具体的用处有什么体现呢
        Dariel:@catcherdream 这就没必要用这个啦!你写个UIView的分类,能够在view和cell中拿到当前的控制器,然后去搞事情
        catcherdream:我觉得的最大的方便就是随便在view cell里都可以push。。
      • 晨风说产品:以前也做过类似的功能,其实很多时候都是通过一个链接在safari打开然后就可以跳到app的指定页面,所以还是比较方便好用的,如果只是为了做app之间页面的跳转那就没什么必要了。
      • godL:既然是个单例为什么要用类方法。 那单例存在的意义呢
        Dariel:@godL 两者之间没有啥必然关系吧!单例是为了避免重复创建对象,方便存储操作数据;类方法用来做封装,方便使用
      • Dimon_Hu:66666666
      • JunpuChen:presentURLString的时候如何添加导航栏
        Dariel:@GeekJunChen 哥们,你把我文章看一遍就知道了:joy:
        JunpuChen:@Dariel在杭州 能具体点吗?哪个方法
        Dariel:@GeekJunChen 有个方法只要把导航控制器的类名传进去就OK
      • 我叫阿水:正常的界面跳转逻辑都是根据产品需要来的,这个用于js交互跳转就不错 :smile:
        Dariel:@YioMidd :grin:
      • 许小帅_allen: if (rootVC.presentedViewController) {
        while (times > 0) {
        rootVC = rootVC.presentingViewController;
        times -= 1;
        }
        [rootVC dismissViewControllerAnimated:YES completion:completion];
        }else {
        NSAssert(0, @"确定能dismiss掉这么多控制器?");
        }

        这段的 Assert 应该在 while 之后?
        我系哆啦:我用你的思路去dismiass多层的时候,发现rootVC不管是自己还是上一层的,dismiss都只能回上一层的控制器 :sweat:
        b7ac9371e916:pop和dismiss过多的控制器iOS SDK不会报错吧?如果不会报错,给个warning就行了,直接崩溃掉有点硬了,本身就是为了解决复杂多变的跳转逻辑,在这里画蛇添足给了个限制实属不该,有时候开发者自己都不知道自己present了几个Vc,首先保证业务逻辑能走通,有时间再去修这个warning,没时间也不至于完不成任务。
        Dariel:@许小帅_allen 那个地方要改为 if (rootVC) {
        while (times > 0) {
        rootVC = rootVC.presentingViewController;
        times -= 1;
        }
        [rootVC dismissViewControllerAnimated:YES completion:completion];
        }

        if (!rootVC.presentedViewController) {
        NSAssert(0, @"确定能dismiss掉这么多控制器?");
        }
        :smile:
      • KA_STEM:个人觉得runtime解决足矣
        Dariel:@KA_STEM 这也是种思路,但能不能做到最优还得看具体代码实现,比如说怎么处理服务器给的控制器名称和参数,或者控制器名称发生改变怎么办
        KA_STEM:@Dariel在杭州 1、服务器定义需要跳转的控制器名称字符串、参数。2、可以结合四楼看看利用一些objc方法实现类的创建与注册以及遍历属性的方法
        Dariel:@KA_STEM 求具体实现
      • 不明之人:mark一下
      • pro_cookies:没必要吧。。用NSClassWithStrin就好了,服务器保存字符串,比这简单多了
        ee84a2cd97ab:@pro_cookies 文章你都不看完
        pro_cookies:还涉及传参啊,没涉及到这个,不过可以用获取类的属性列表那个方法,
        Dariel:@pro_cookies 你们公司是这样做的?参数怎么传递?
      • 大牛之路:不错,我是初级开发者,最近正想着app间的页面怎么随意跳转呢。楼主的博文解决了我的问题
        金银岛:我猜你想要表达的正确描述应该是"最近正想着某一个app,其页面与页面之间怎么随意跳转呢"
        大牛之路:@Dariel在杭州 ……我没表达清楚
        Dariel:@大牛之路 app与app之间的跳转得去设置 URL Types吧
      • Roader:厉害,支持!
        Dariel:@Longroader :smile:

      本文标题:iOS使用自定义URL实现控制器之间的跳转

      本文链接:https://www.haomeiwen.com/subject/qokbsttx.html