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

一个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就是对我最好的鼓励.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 前言的前言 唐巧前辈在微信公众号「iOSDevTips」以及其博客上推送了我的文章后,我的 Github 各项指标...
    VincentHK阅读 5,241评论 4 44
  • 1.自定义控件 a.继承某个控件 b.重写initWithFrame方法可以设置一些它的属性 c.在layouts...
    圍繞的城阅读 2,958评论 2 4
  • 概述 这篇文章,我将讲述几种转场动画的自定义方式,并且每种方式附上一个示例,毕竟代码才是我们的语言,这样比较容易上...
    伯恩的遗产阅读 53,610评论 37 379
  • OC开发我们主要有以下三种自定义方法,供大家参考:Push & PopModalSegue 前两种大家都很熟悉,第...
    ScaryMonsterLyn阅读 1,540评论 1 3
  • 今天早上奶奶把我送到了学校,门已经开了,我进去后大家都在玩全员加速中,我便跑了进去一起玩,玩着玩着不知不觉老...
    kanzihan阅读 130评论 0 0