[iOS] 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
ps.DCURLRouter是OC版的,后續看情況可能會有swift版本的.
你的star是對我最好的支持.:smiley:
1.簡單集成
只要把 DCURLRouter 這個文件夾拖到項目中就行了,后續會支持 cocoapods .
2. 簡單配置
- 每一個自定義的URL都會有一個對應的控制器,那Xocde怎么知道呢?我們需要一個plist文件.打開 DCURLRouter.plist 文件
內部結構大概長這樣.除了自定義的URL上面還有 http 和 https ,這是當如果URL是網頁鏈接的時候,DCURLRouter會自動跳轉到自定義好的 webView控制器 ,并把URL當成參數傳遞到webView控制器.是不是很方便. 下面的 dariel 字典就是用來存放自定義URL以及對應的控制器名稱的. dariel 就是自定義協議頭了.以后就可以把自定義的URL和對應的控制器放這里了. - 加載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控制器跳轉就不一定要在控制器中進行了.
-
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];
-
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原理
- 跳轉前我們需要為自定義的URL,設置一個對應的控制器.然后在對應的控制器中執行push操作,就能夠push到對應的控制器了.
[DCURLRouter pushURLString:@"dariel://threeitem" animated:YES];
-
執行完上面一句代碼,經過一些簡單處理,最后會來到這里. #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創建成對應的控制器.具體啥的寫的很明白了,就不詳細說了啊!
-
傳參的接收
注意到上面的 [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); }
- 在 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]; }
-
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; } // 如果導航控制器不存在,就會創建一個新的,設置為根控制器 } } }
代碼寫的很詳細,就不詳細說了啊!
- 大概同理,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多層控制器的實現原理.
-
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 這個方法.這樣就能做到了.
-
dismiss控制器
+ (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掉這么多控制器?"); } }
dismissViewController這個的實現思路就有點特別了,因為沒有辦法拿到所有的modal出來的控制器,只能拿到上一個,所以這邊就是用的while循環實現的.
5.總結
大概講了下具體的使用和大概功能的實現,還有很多具體實現細節,有興趣的童鞋可以看給出的源碼!
DCURLRouter組件源碼: https://github.com/DarielChen/DCURLRouter
歡迎使用,歡迎star,你的star就是對我最好的鼓勵.
來自:http://www.jianshu.com/p/36a43202b0cd