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

ColemanVerg 8年前
   <p>一个app往往有很多界面,而界面之间的跳转也就是对应控制器的跳转,控制器的跳转一般有两种情况 push 或者 modal,push 和 modal 的默认效果是系统提供的,但也可以自定义.有兴趣了解一下自定义的童鞋可以看这篇, <a href="/misc/goto?guid=4959677027213448831" rel="nofollow,noindex">iOS动画指南 - 6.可以很酷的转场动画</a> .</p>    <p><img src="https://simg.open-open.com/show/31757abb76ace4831f29beb0dd509515.jpg"></p>    <p>文章配图</p>    <h2>1. 概述</h2>    <p>系统提供的push和modal方法有时并不能满足实际需求.比如,我们需要根据服务器返回的字段跳到指定的控制器,难道作判断吗?那显然不是最佳解决方案.</p>    <p>其实我们可以这样:</p>    <pre>  <code class="language-swift">NSString *urlStr = @"dariel://twoitem?name=dariel&userid=213213";        // push      [DCURLRouter pushURLString:urlStr animated:YES];        // modal      [DCURLRouter presentURLString:urlStr animated:YES completion:nil];</code></pre>    <p>对的,就是通过自定义URL+拼接参数,实现跳转.当然啦, <a href="/misc/goto?guid=4959677027304278372" rel="nofollow,noindex">DCURLRouter</a> 的功能远不止这点.</p>    <h2>2.DCURLRouter的基本使用</h2>    <p>DCURLRouter是一个通过简单配置就能够实现自定义URL跳转的开源组件: <a href="/misc/goto?guid=4959677027304278372" rel="nofollow,noindex">GitHub</a></p>    <p>ps.DCURLRouter是OC版的,后续看情况可能会有swift版本的.</p>    <p>你的star是对我最好的支持.:smiley:</p>    <p>1.简单集成</p>    <p>只要把 DCURLRouter 这个文件夹拖到项目中就行了,后续会支持 cocoapods .</p>    <p>2. 简单配置</p>    <ol>     <li>每一个自定义的URL都会有一个对应的控制器,那Xocde怎么知道呢?我们需要一个plist文件.打开 DCURLRouter.plist 文件 <p><img src="https://simg.open-open.com/show/e1985ef903af59e2a3ed33ad57c2005d.png"></p> 内部结构大概长这样.除了自定义的URL上面还有 http 和 https ,这是当如果URL是网页链接的时候,DCURLRouter会自动跳转到自定义好的 webView控制器 ,并把URL当成参数传递到webView控制器.是不是很方便. 下面的 dariel 字典就是用来存放自定义URL以及对应的控制器名称的. dariel 就是自定义协议头了.以后就可以把自定义的URL和对应的控制器放这里了.</li>     <li>加载DCURLRouter.plist文件数据 <pre>  <code class="language-swift">- (BOOL)application:(UIApplication *)application  didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {      [DCURLRouter loadConfigDictFromPlist:@"DCURLRouter.plist"];      return YES;  }</code></pre> </li>    </ol>    <p>3. push和modal的使用</p>    <p>所有的push和modal方法都可以通过DCURLRouter这个类方法来调用.这样在push和modal的时候就不需要拿到导航控制器或控制器再跳转了.也就是说,以后push和modal控制器跳转就不一定要在控制器中进行了.</p>    <ol>     <li> <p>push控制器</p> <pre>  <code class="language-swift">// 不需要拼接参数直接跳转   [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];</code></pre> </li>     <li> <p>modal控制器</p> <p>用法和push差不多,只是这里添加了一个给modal出来的控制器加一个导航控制器的方法.</p> <pre>  <code class="language-swift">// 不需要拼接参数直接跳转   [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];</code></pre> </li>    </ol>    <p>4. 后退 pop 和 dismiss</p>    <p>在实际开发中,好几次的界面的跳转组成了一个业务流程,整个业务流程结束后通常会要求返回最开始的界面,这就要让控制器连续后退好几次,但苹果是没有提供方法的.DCURLRouter给出了具体的实现方案.</p>    <p>pop:</p>    <pre>  <code class="language-swift">/** pop掉一层控制器 */      + (void)popViewControllerAnimated:(BOOL)animated;      /** pop掉两层控制器 */      + (void)popTwiceViewControllerAnimated:(BOOL)animated;      /** pop掉times层控制器 */      + (void)popViewControllerWithTimes:(NSUInteger)times animated:(BOOL)animated;      /** pop到根层控制器 */      + (void)popToRootViewControllerAnimated:(BOOL)animated;</code></pre>    <p>dismiss:</p>    <pre>  <code class="language-swift">/** 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;</code></pre>    <p>5.参数的接收,以及其它方法</p>    <p>在3中如果在自定义了URL后面拼接了参数,或者用字典传递了参数,那么在目的控制器怎么接收呢?其实参数的接收很简单.只要导入这个分类 #import "UIViewController+DCURLRouter.h" 就行了,然后就能拿到这三个参数.</p>    <pre>  <code class="language-swift">NSLog(@"接收的参数%@", self.params);      NSLog(@"拿到URL:%@", self.originUrl);      NSLog(@"URL路径:%@", self.path);</code></pre>    <p>但有时我们我需要把值传递给发送push或者modal方的控制器,也就是逆传,也很简单,可以用代理或者block.有方法可以拿到当前的控制器,以及导航控制器</p>    <pre>  <code class="language-swift">// 拿到当前控制器      UIViewController *currentController = [DCURLRouter sharedDCURLRouter].currentViewController;      // 拿到当前控制器的导航控制器      UINavigationController *currentNavgationController = [DCURLRouter sharedDCURLRouter].currentNavigationViewController;</code></pre>    <p>至此怎么使用就说完了,不知道感觉怎样呢?</p>    <h2>3.DCURLRouter自定义URL跳转的的实现原理.</h2>    <p>1.文件结构</p>    <p>首先看一下几个文件分别是干什么用的?</p>    <p><img src="https://simg.open-open.com/show/007d67070e4cec7c5b0d5419e47bdb68.png"></p>    <ul>     <li>DCURLRouter是个单例,是主要类,所有对外的接口都是由它提供.我们就是用它通过调用类方法来实现自定义URL跳转的.</li>     <li>DCURLNavgation也是单例,主要是用来重写和自定义系统的跳转方法.</li>     <li>UIViewController+DCURLRouter 是UIViewController的分类,用于接收控制器的参数,以及用来创建控制器的.</li>     <li>DCSingleton 单例的宏 只要在需要创建单例的类中分别导入.h文件中 DCSingletonH(类名) .m文件中 DCSingletonM(类名) ,这样就可以很方便的创建单例了.具体看代码.</li>     <li>DCURLRouter.plist 就是用来存放与自定义URL对应的控制器名称的.</li>    </ul>    <p>2.一个自定义URL字符串的push原理</p>    <ol>     <li>跳转前我们需要为自定义的URL,设置一个对应的控制器.然后在对应的控制器中执行push操作,就能够push到对应的控制器了. <p><img src="https://simg.open-open.com/show/8d2b33549f57d897fec741839d8f8174.png"></p> <pre>  <code class="language-swift">[DCURLRouter pushURLString:@"dariel://threeitem" animated:YES];</code></pre> </li>     <li> <p>执行完上面一句代码,经过一些简单处理,最后会来到这里. #import "UIViewController+DCURLRouter.h" 的这个方法中</p> <pre>  <code class="language-swift">+ (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;  }</code></pre> <p>在这个方法中将自定义URL创建成对应的控制器.具体啥的写的很明白了,就不详细说了啊!</p> </li>     <li> <p>传参的接收</p> <p>注意到上面的 [VC open:url withQuery:query]; 吗?是在下面这个方法中完成赋值的,但我们都有个常识,怎么在分类中保存属性呢?</p> <pre>  <code class="language-swift">- (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];   }  }</code></pre> <p>答案是利用 runtime , runtime 可以为我们做好这个.</p> <pre>  <code class="language-swift">- (void)setOriginUrl:(NSURL *)originUrl {    // 为分类设置属性值   objc_setAssociatedObject(self, &URLoriginUrl,                            originUrl,                            OBJC_ASSOCIATION_RETAIN_NONATOMIC);  }  - (NSURL *)originUrl {   // 获取分类的属性值   return objc_getAssociatedObject(self, &URLoriginUrl);  }</code></pre> </li>     <li>在 DCURLRouter 方法中我们可以拿到在2中返回的VC,然后我们需要到DCURLNavgation中调用push方法了 <pre>  <code class="language-swift">+ (void)pushURLString:(NSString *)urlString animated:(BOOL)animated {   UIViewController *viewController = [UIViewController initFromString:urlString fromConfig:[DCURLRouter sharedDCURLRouter].configDict];   [DCURLNavgation pushViewController:viewController animated:animated replace:NO];  }</code></pre> </li>     <li> <p>DCURLNavgation中怎样去处理push</p> <pre>  <code class="language-swift">+ (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;           } // 如果导航控制器不存在,就会创建一个新的,设置为根控制器       }   }  }</code></pre> <p>代码写的很详细,就不详细说了啊!</p> </li>     <li>大概同理,DCURLNavgation中怎样去处理modal <pre>  <code class="language-swift">+ (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;       }   }  }</code></pre> 代码也很详细,有问题可以在下面留言!</li>    </ol>    <h2>4. 怎样去加载一个自定义的webView控制器</h2>    <p>在上面3.2.2中,不知道有没有注意到那个对网络地址的处理</p>    <pre>  <code class="language-swift">// 处理网络地址的情况   if ([url.scheme isEqualToString:@"http"] || [url.scheme isEqualToString:@"https"]) {   class = NSClassFromString([configDict objectForKey:url.scheme]);   VC.params = @{@"urlStr": [url absoluteString]};</code></pre>    <p>如果协议头是http或者https的情况,我们可以通过[configDict objectForKey:url.scheme]拿到自定义webView控制器的名称,然后再去创建webView控制器,之后我们是将url通过参数传到webView控制器中,最后在webView控制器中加载对应的webview.</p>    <h2>5.关于怎样一次性pop和dismiss多层控制器的实现原理.</h2>    <ol>     <li> <p>pop控制器</p> <pre>  <code class="language-swift">+ (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掉那么多控制器?");           }       }   }  }</code></pre> <p>popViewController 实现的思路比较简单,因为可以拿到导航控制器上的所有控制器,然后通过 objectAtIndex 这个方法.这样就能做到了.</p> </li>     <li> <p>dismiss控制器</p> <pre>  <code class="language-swift">+ (void)dismissViewControllerWithTimes:(NSUInteger)times animated: (BOOL)flag completion: (void (^ __nullable)(void))completion {   UIViewController *rootVC = [[DCURLNavgation sharedDCURLNavgation] currentViewController];     if (rootVC.presentedViewController) {       while (times > 0) {           rootVC = rootVC.presentingViewController;           times -= 1;       }       [rootVC dismissViewControllerAnimated:YES completion:completion];   }else {       NSAssert(0, @"确定能dismiss掉这么多控制器?");   }  }</code></pre> <p>dismissViewController这个的实现思路就有点特别了,因为没有办法拿到所有的modal出来的控制器,只能拿到上一个,所以这边就是用的while循环实现的.</p> </li>    </ol>    <h2>5.总结</h2>    <p>大概讲了下具体的使用和大概功能的实现,还有很多具体实现细节,有兴趣的童鞋可以看给出的源码!</p>    <p>DCURLRouter组件源码: <a href="/misc/goto?guid=4959677027304278372" rel="nofollow,noindex">https://github.com/DarielChen/DCURLRouter</a></p>    <p>欢迎使用,欢迎star,你的star就是对我最好的鼓励.</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/36a43202b0cd</p>    <p> </p>    <p><span style="background:rgb(189, 8, 28) url("data:image/svg+xml; border-radius:2px; border:medium none; color:rgb(255, 255, 255); cursor:pointer; display:none; font:bold 11px/20px "Helvetica Neue",Helvetica,sans-serif; left:30px; opacity:0.85; padding:0px 4px 0px 0px; position:absolute; text-align:center; text-indent:20px; top:123px; width:auto; z-index:8675309">Save</span></p>