此篇文章偏向實戰(zhàn), 想深入學習 Router 思想的推薦霜神寫的 iOS 組件化 —— 路由設計思路分析扒秸。
==Demo 在文章最??==
為什么 Router
路由基礎三問, 每次接觸新穎思想框架時, 我都會不禁的問自己這幾個問題, 希望通過下面幾個簡要的概括, 能很好的幫助大家理解 Router;
- 路由是什么,解決了什么問題
[圖片上傳失敗...(image-d9aae4-1612352196155)] 來解決 App 內(nèi)外所有頁面之間的跳轉邏輯, 經(jīng)過這幾年的學習和使用, 將其記錄一邊鞏固知識, 寫出來跟大家一起學習, 加上看到網(wǎng)上分享關于使用組件化-Router 相關文章偏于理論, 很少有完整詳細Demo, 具體在項目中使用還需進一步深入研究, 所以有了此篇文章, 有什么不對或需要補充的, 望大家多多指教。
此篇文章偏向實戰(zhàn), 想深入學習 Router 思想的推薦霜神寫的 iOS 組件化 —— 路由設計思路分析逗威。
==Demo 在文章最??==
為什么 Router
路由基礎三問, 每次接觸新穎思想框架時, 我都會不禁的問自己這幾個問題, 希望通過下面幾個簡要的概括, 能很好的幫助大家理解 Router;
- 路由是什么,解決了什么問題
上面一幅圖很形象的展示了項目中各個控制器模塊之間錯綜復雜的關系, 當我們在處理不當?shù)那闆r下可能更加糟糕.
使用 Router 之后大概是這樣的;
打個比方, Router 就是跟我們?nèi)粘J褂玫穆酚善饕粯? App 內(nèi)每個控制器可以想象成已經(jīng)連接了這臺路由器的不同設備, 當然連接路由器時, 一般需要輸入密碼, Router 同樣的, 使用前需要每臺設備進行一次注冊, Router 在內(nèi)部保存每臺設備的 URL, 不同設備之間需要交互時, 將消息發(fā)送到路由器中統(tǒng)一處理;
當控制器之間需要交互跳轉時, 只需要將對應的 URL 地址發(fā)送到 Router 里, Router 根據(jù)其注冊的 URL 來尋址到對方信息, 然后負責實例化對象, 并傳參, 進行跳轉等工作, 各個控制器之間不需要相互依賴對方, 完美解決不同模塊之間耦合构眯!
-
為什么要用路由來實現(xiàn) VC 跳轉
Router 能做的事情很多, 首先我們用它來解決棘手的控制器耦合關系,是一種非常有效的解決方案;
在 App 中控制器跳轉普遍分為 3 種, 模態(tài)跳轉Modal(presented/dismiss), 導航控制器跳轉(Push/pop), Storyboard 跳轉(Segue), 還有 UITabBarVC 主控制器 Index 切換;
除了常規(guī)的控制器之間跳轉之外, 還會有 3D Touch 指定跳轉到某個控制器中;
App 之間跳轉: URL Scheme, Universal Links方式;
可想而知 App 內(nèi)不管是頁面切換, 外部調用, 都會涉及到控制器的跳轉, 切換等等;
下面引用常見場景來舉個栗子:
Router 前 偽代碼:
假如在沒有引入 Router 之前, 實現(xiàn) A Push B, B Modal C 的場景: 一般做法都是在 A 中引入B, B 中引入 C, 然后在每次跳轉前都需要來一段硬編碼,
//A Push B A 頁面跳轉至 B頁面, 并且設置相應 @perpeoty, callback 等;
#import "B"
B* BVC = [B new];
BVC.delegate = A;
BVC.name = @"jersey";
BVC.callback = ^ (id data) {
};
...
...
... 對 b 設置一些業(yè)務相關參數(shù), delegate, callback 等等;
[A.nav pushVC: BVC animation: true];
// B -> C
#import "C"
C* CVC = [C new];
[B presentVC: CVC];
[B presentVC: CVC animation: true completion: nil];
==Router 后 偽代碼:==
在引用了 Router 之后, 相同的場景下, 我們的代碼是這樣的; 在需要做跳轉的控制器引入我們封裝好的 ==JSDVCRouter(是針對 JLRouter 進行的一層封裝, 專門用于管理 App 跳轉的類, 在文章后面會詳細講解)== 即可.
// A Push B;
#import "JSDVCRouter"
[JSDVCRouter openURL: BVCPath info: @{@"delegate":self,@"name":@"jersey",@"callback":callback}];
// BVCPath: 表示我們對 B 控制器定義的路徑, 一般保存在全局 Map 里面, 每個 Path 映射當前控制器 Map 包含相關 title, class, needLog, 等參數(shù);
// B Modal C
[JSDVCRouter openURL: C info: {kJSDRouteSegue: @"Modal"}]; // 控制器之間跳轉默認以 Push 實現(xiàn), 當需要 Modal 時, 則傳遞一個參數(shù);
看到這里相信認真閱讀的同學們已看出使用 Router 的好處:
1. 耦合度降低: A 控制器不需要知道 B 控制器的存在, 只需要 import "JSDRouter", 由其去進行相應跳轉邏輯, 以及賦值等等;
2. 代碼閱讀性提高: 當然在剛剛接觸時, 看著會不大不習慣, 等接觸一段時間之后, 不僅減少了代碼行數(shù), 同時可讀性還是很高的, 跟 push/pop, present/dismiss 說再見吧;
3. 提高代碼復用性: 每次控制器之間跳轉和賦值等操作, 都需要重復性的 code 一次(嚴重違背了: 可復用性原則), 通過 JSDRouter 將跳轉和賦值等邏輯封裝起來, 一次 code, 終生受用;
4. 易于維護: 寫到這一點有點兒糾結, 當項目隨著公司規(guī)模不斷壯大時, 控制器數(shù)量, 跳轉變得越加復雜, 跳轉方法和邏輯很容易變得越來越混亂, 后期管理起來比較困難寥裂。 使用 JSDVCRouter 單一職責的原則來專門負責 App 內(nèi)所有的跳轉, 能非常有效的提高測試及后期維護, 當然成本是需要維護 RouterMap 同時完善 JSDVCRouter 內(nèi)部邏輯;
5. 動態(tài)化及靈活性: 使用 Router 時可以配合后臺響應傳遞響應的 Key 來決定真正跳轉的頁面, 而不是硬編碼的方式來進行跳轉;
6. 待補充:
-
實現(xiàn) Router 完成控制器跳轉, 至少需要幾個步驟趁舀?
首次將控制器跳轉轉成 Router 方案
很簡單只有 3個步驟, 如何需求變動不大的話, 幾乎一勞永逸赖捌;
- Map 表創(chuàng)建: 其是一個全局 Map, App 內(nèi)相應的控制器定義好 Path, Router 可以根據(jù) Path 映射相應控制器制定的 Map 內(nèi), Map 里面最少包含當前控制器的參數(shù)如: {@"Class": @"控制器類名"}。相當于調用這個路由時,得到一組其綁定的 Map 作為參數(shù), 通過 Class 來初始化實例;
代碼結構如:
+ (NSDictionary *)configInfo
return @{ JSDRouteHomeCenter: @{
@"class": @"JSDAHomeCenterVC",
@"name": @"首頁",
@"parameter": @"",
@"needLogin": @"0", },
JSDRouteUserLogin: @{
@"class": @"JSDUserLoginVC",
@"name": @"登陸",
@"parameter": @"",
@"needLogin": @"0", },
};
- 封裝 JLRouter; 為了方便使用,管理,以及后期遷移等!類似使用 AFNetwork, SDWebImage, MJRefresh 等有名的開源庫一樣, 由于開源庫提供功能非常豐富, 但是可能我們實際使用到的只是它一兩個主要的功能來解決項目中存在的問題, 大家都會根據(jù)公司具體的業(yè)務場景或者使用習慣, 來對其進行一層甚至多層封裝一樣, 使其能更加適合實際要求;
筆者對其進行了一層封裝 + Category 的形式: JSDVCRouter,JSDVCRouter + Add;
JSDVCRouter: 主要用于聲明 Router 調用接口;
JSDVCRouter + Handle: 主要用于實現(xiàn) Router 注冊, 處理控制器之間跳轉和參數(shù)賦值代碼; - 根據(jù)約定 Path 進行跳轉: 上面 1 2 都準備好之后, 即可輕松的進行控制器跳轉 [JSDVCRouter openURL:BVC];
業(yè)務變更后期維護
- Map 維護: 隨著業(yè)務發(fā)展, 當有新的頁面加入時, 對 Map 添加一個指定的 Path 和綁定的相應參數(shù);
- JSDVCRouter 維護: 其包含著真正對控制器初始化跳轉和賦值的代碼這里一般很少進行修改; 比如后期需支持跳轉到 H5, 處理 3D Touch, Universal Links 時來這里進行維護;
實戰(zhàn) Code矮烹!
寫到這里, 筆者不知道上面講的對 Router 實現(xiàn)控制器跳轉的簡要介紹, 是否起到幫助初步接觸 Router 時的同學們, 希望下面通過 Code 的方式能讓大家更好的理解和使用起來!
下面詳細介紹筆者封裝 JLRoutes 實現(xiàn)控制器跳轉的三個類:
JSDVCRouterConfig
這個文件主要用于管理所有 Router 映射到指定控制器類名(class), 以及相關參數(shù)的配置文件(title,needLogin等), 具體配置根據(jù)實際項目需求進行即可;
- 為了編譯期能更好的檢查到錯誤, 使用 extern NSString* const 聲明, 配合 NSString* const 實現(xiàn)指定 Router URL, 使用的時候直接通過外部聲明的常量字符串來指定跳轉即可;
- 這樣管理 Router URL 能更加方便閱讀和維護, 如果直接使用 @"/login" 的方式來進行綁定可讀性差, 很容易出現(xiàn)粗心大意導致的錯誤;
代碼如下:
//App 內(nèi)所有控制器
extern NSString* const JSDVCRouteWebview;
extern NSString* const JSDVCRouteLogin;
@interface JSDVCRouterConfig : NSObject
+ (NSDictionary *)configMapInfo;
@end
//App 內(nèi)相關控制器
NSString* const JSDVCRouteWebview = @"/webView";
NSString* const JSDVCRouteLogin = @"/login";
@implementation JSDVCRouterConfig
+ (NSDictionary *)configMapInfo {
return @{
JSDVCRouteWebview: @{@"class": @"JSDWebViewVC",
@"title": @"WebView",
@"flags": @"",
@"needLogin": @"",
},
JSDVCRouteLogin: @{@"class": @"JSDLoginVC",
@"title": @"登錄",
@"flags": @"",
@"needLogin": @"",
},
};
@end
JSDVCRouter
這個類內(nèi)部實現(xiàn)的事情非常簡單, 繼承自 NSObject, 對外提供 注冊和調用 Router 接口, 在內(nèi)部調用 JLRoutes 提供的接口;
在項目中所有跳轉均使用此類提供的接口來調用 Router;
一個是默認不帶任何參數(shù)
另一個可以攜帶我們需要的參數(shù)(NSDictionary);
[JSDVCRouter openURL:JSDVCRouteAppear]; //push 到 AppearVC;
[JSDVCRouter openURL:JSDVCRouteAppear parameters:@{kJSDVCRouteSegue: kJSDVCRouteSegueModal, @"name": @"jersey"}]; // Modal 到 Appear VC 并攜帶參數(shù) name;
單獨封裝一個 JSDVCRouter 好處:
防止三方庫入侵. 其繼承自 NSObject 并不直接依賴于 JLRouter, 這樣在后期如果考慮更換三方庫, 或者自己封裝一套類似 JLRouter 提供的功能時, 只需要對其修改即可, 其他地方均無需修改;
接口隔離保持統(tǒng)一, 可讀性更高;
@interface JSDVCRouter : NSObject
+ (BOOL)openURL:(NSString *)url;//調用 Router;
+ (BOOL)openURL:(NSString *)url parameters:(NSDictionary *)parameters;
+ (void)addRoute:(NSString* )route handler:(BOOL (^)(NSDictionary *parameters))handlerBlock;//注冊 Router,調用 Router 時會觸發(fā)回調;
@end
#define JSDRouterURL(string) [NSURL URLWithString:string]
@implementation JSDVCRouter
+ (BOOL)openURL:(NSString *)url {
return [self routeURL:url parameters:nil];
}
+ (BOOL)openURL:(NSString *)url parameters:(NSDictionary *)parameters {
return [self routeURL:url parameters:parameters];
}
+ (void)addRoute:(NSString *)route handler:(BOOL (^)(NSDictionary * _Nonnull parameters))handlerBlock {
[JLRoutes addRoute:route handler:handlerBlock];
}
#pragma mark - mark JLRouter
+ (BOOL)routeURL:(NSString*)url parameters:(NSDictionary *)parameters{
return [JLRoutes routeURL:JSDRouterURL(url) withParameters:parameters];
}
@end
JSDVCRouter+Handle
真正注冊和調用 Router 時處理回調控制器跳轉和參數(shù)賦值邏輯實現(xiàn)放在這里越庇。
注冊 Router : 對控制器內(nèi)所有 Router 一一進行注冊以及 TabBarIndex 切換和 處理返回 Router, 將回調統(tǒng)一轉發(fā)到定義的方法里頭罩锐。
處理 Router: 也就是注冊好 Router 之后, 調用相應 Router 時, 我們在注冊時寫得回調方法, 這里是執(zhí)行控制器跳轉和傳參的邏輯。
關于控制器跳轉: 在觸發(fā) Router 時, 我們能拿到 Router 映射到的 Map, 獲取到其 Class, 在通過 Class 來進行初始初始化實例, 這里通過對 UIViewController Category 找到當前 visibleVC 來進行 Push 或 Modal, 我們也可以根據(jù)業(yè)務方傳遞過來的參數(shù)來決定進行 Push 或 Modal 以及是否需要執(zhí)行動畫等等;
關于傳參: 傳遞過來的參數(shù)是字典的數(shù)據(jù)結構, 所以我們先檢測實例 VC 是否包含這個屬性, [vc respondsToSelector:NSSelectorFromString(key)], 如果 VC 有這個屬性則直接使用 KVC 的方式來進行賦值, 為了防止在開發(fā)時, 傳入的字典 Key 與 VC 屬性不匹配導致一些 Bug, 添加一層 NSAssert,這樣能在開發(fā)過程中更快找到問題卤唉!
筆者自行封裝的控制器跳轉邏輯可能有考慮不周的地方, 主要還得根據(jù)具體業(yè)務需求來做具體判斷;
下面分別是注冊 Router 和匹配到 Router 之后回調處理代碼, 有點長請耐心閱讀
Router 注冊, 將三種類型回調處理統(tǒng)一
@implementation JSDVCRouter (Handle)
//注冊 Router, 控制器的跳轉 + UITabBarIndex 切換 + 頁面返回
+ (void)load {
[self performSelectorOnMainThread:@selector(registerRouter) withObject:nil waitUntilDone:false];
}
+ (void)registerRouter {
//獲取全局 RouterMapInfo
NSDictionary* routerMapInfo = [JSDVCRouterConfig configMapInfo];
// router 對應控制器路徑, 使用其來注冊 Route, 當調用當前 Route 時會執(zhí)行回調; 回調參數(shù) parameters: 在執(zhí)行 Route 時傳入的參數(shù);
for (NSString* router in routerMapInfo.allKeys) {
NSDictionary* routerMap = routerMapInfo[router];
NSString* className = routerMap[kJSDVCRouteClassName];
if (JSDIsString(className)) {
/*注冊所有控制器 Router, 使用 [JSDVCRouter openURL:JSDVCRouteAppear]; push 到 AppearVC;
[JSDVCRouter openURL:JSDVCRouteAppear parameters:@{kJSDVCRouteSegue: kJSDVCRouteSegueModal, @"name": @"jersey"}]; Modal 到 Appear VC 并攜帶參數(shù) name;
*/
[self addRoute:router handler:^BOOL(NSDictionary * _Nonnull parameters) {
//執(zhí)行路由匹配成功之后,跳轉邏輯回調;
/*執(zhí)行 Route 回調; 處理控制器跳轉 + 傳參;
** routerMap: 當前 route 映射的 routeMap; 我們在 RouterConfig 配置的 Map;
** parameters: 調用 route 時, 傳入的參數(shù);
*/
return [self executeRouterClassName:className routerMap:routerMap parameters:parameters];
}];
}
}
// 注冊 Router 到指定TabBar Index; 使用 [JSDVCRouter openURL:JSDVCRouteCafeTab] 切換到 Cafe Index
[self addRoute:@"/rootTab/:index" handler:^BOOL(NSDictionary * _Nonnull parameters) {
NSInteger index = [parameters[@"index"] integerValue];
// 處理 UITabBarControllerIndex 切換;
UITabBarController* tabBarVC = (UITabBarController* )[UIViewController jsd_rootViewController];
if ([tabBarVC isKindOfClass:[UITabBarController class]] && index >= 0 && tabBarVC.viewControllers.count >= index) {
UIViewController* indexVC = tabBarVC.viewControllers[index];
if ([indexVC isKindOfClass:[UINavigationController class]]) {
indexVC = ((UINavigationController *)indexVC).topViewController;
}
//傳參
[self setupParameters:parameters forViewController:indexVC];
tabBarVC.selectedIndex = index;
return YES;
} else {
return NO;
}
}];
// 注冊返回上層頁面 Router, 使用 [JSDVCRouter openURL:kJSDVCRouteSegueBack] 返回上一頁 或 [JSDVCRouter openURL:kJSDVCRouteSegueBack parameters:@{kJSDVCRouteBackIndex: @(2)}] 返回前兩頁
[self addRoute:kJSDVCRouteSegueBack handler:^BOOL(NSDictionary * _Nonnull parameters) {
return [self executeBackRouterParameters:parameters];
}];
}
Router 匹配到之后回調: 實例化控制器, 參數(shù)賦值, 頁面跳轉
#pragma mark - execute Router VC
// 當查找到指定 Router 時, 觸發(fā)路由回調邏輯; 找不到已注冊 Router 則直接返回 NO; 如需要的話, 也可以在這里注冊一個全局未匹配到 Router 執(zhí)行的回調進行異常處理;
+ (BOOL)executeRouterClassName:(NSString *)className routerMap:(NSDictionary* )routerMap parameters:(NSDictionary* )parameters {
// 攔截 Router 映射參數(shù),是否需要登錄才可跳轉;
BOOL needLogin = [routerMap[kJSDVCRouteClassNeedLogin] boolValue];
if (needLogin && !userIsLogin) {
[JSDVCRouter openURL:JSDVCRouteLogin];
return NO;
}
//統(tǒng)一初始化控制器,傳參和跳轉;
UIViewController* vc = [self viewControllerWithClassName:className routerMap:routerMap parameters: parameters];
if (vc) {
[self gotoViewController:vc parameters:parameters];
return YES;
} else {
return NO;
}
}
// 根據(jù) Router 映射到的類名實例化控制器;
+ (UIViewController *)viewControllerWithClassName:(NSString *)className routerMap:(NSDictionary *)routerMap parameters:(NSDictionary* )parameters {
id vc = [[NSClassFromString(className) alloc] init];
if (![vc isKindOfClass:[UIViewController class]]) {
vc = nil;
}
#if DEBUG
//vc不是UIViewController
NSAssert(vc, @"%s: %@ is not kind of UIViewController class, routerMap: %@",__func__ ,className, routerMap);
#endif
//參數(shù)賦值
[self setupParameters:parameters forViewController:vc];
return vc;
}
// 對 VC 參數(shù)賦值
+ (void)setupParameters:(NSDictionary *)params forViewController:(UIViewController* )vc {
for (NSString *key in params.allKeys) {
BOOL hasKey = [vc respondsToSelector:NSSelectorFromString(key)];
BOOL notNil = params[key] != nil;
if (hasKey && notNil) {
[vc setValue:params[key] forKey:key];
}
#if DEBUG
//vc沒有相應屬性涩惑,但卻傳了值
if ([key hasPrefix:@"JLRoute"]==NO &&
[key hasPrefix:@"JSDVCRoute"]==NO && [params[@"JLRoutePattern"] rangeOfString:[NSString stringWithFormat:@":%@",key]].location==NSNotFound) {
NSAssert(hasKey == YES, @"%s: %@ is not property for the key %@",__func__ ,vc,key);
}
#endif
};
}
// 跳轉和參數(shù)設置;
+ (void)gotoViewController:(UIViewController *)vc parameters:(NSDictionary *)parameters {
UIViewController* currentVC = [UIViewController jsd_findVisibleViewController];
NSString *segue = parameters[kJSDVCRouteSegue] ? parameters[kJSDVCRouteSegue] : kJSDVCRouteSeguePush; // 決定 present 或者 Push; 默認值 Push
BOOL animated = parameters[kJSDVCRouteAnimated] ? [parameters[kJSDVCRouteAnimated] boolValue] : YES; // 轉場動畫;
NSLog(@"%s 跳轉: %@ %@ %@",__func__ ,currentVC, segue,vc);
if ([segue isEqualToString:kJSDVCRouteSeguePush]) { //PUSH
if (currentVC.navigationController) {
NSString *backIndexString = [NSString stringWithFormat:@"%@",parameters[kJSDVCRouteBackIndex]];
UINavigationController* nav = currentVC.navigationController;
if ([backIndexString isEqualToString:kJSDVCRouteIndexRoot]) {
NSMutableArray *vcs = [NSMutableArray arrayWithObject:nav.viewControllers.firstObject];
[vcs addObject:vc];
[nav setViewControllers:vcs animated:animated];
} else if ([backIndexString integerValue] && [backIndexString integerValue] < nav.viewControllers.count) {
//移除掉指定數(shù)量的 VC, 在Push;
NSMutableArray *vcs = [nav.viewControllers mutableCopy];
[vcs removeObjectsInRange:NSMakeRange(vcs.count - [backIndexString integerValue], [backIndexString integerValue])];
nav.viewControllers = vcs;
[nav pushViewController:vc animated:YES];
} else {
[nav pushViewController:vc animated:animated];
}
}
else { //由于無導航欄, 直接執(zhí)行 Modal
BOOL needNavigation = parameters[kJSDVCRouteSegueNeedNavigation] ? NO : YES;
if (needNavigation) {
UINavigationController* navigationVC = [[UINavigationController alloc] initWithRootViewController:vc];
//vc.modalPresentationStyle = UIModalPresentationFullScreen;
[currentVC presentViewController:navigationVC animated:YES completion:nil];
}
else {
//vc.modalPresentationStyle = UIModalPresentationFullScreen;
[currentVC presentViewController:vc animated:animated completion:nil];
}
}
}
else { //Modal
BOOL needNavigation = parameters[kJSDVCRouteSegueNeedNavigation] ? parameters[kJSDVCRouteSegueNeedNavigation] : NO;
if (needNavigation) {
UINavigationController* navigationVC = [[UINavigationController alloc] initWithRootViewController:vc];
//vc.modalPresentationStyle = UIModalPresentationFullScreen;
[currentVC presentViewController:navigationVC animated:animated completion:nil];
}
else {
//vc.modalPresentationStyle = UIModalPresentationFullScreen;
[currentVC presentViewController:vc animated:animated completion:nil];
}
}
}
能堅持看到這里, 應該對 Router 進行控制器跳轉已經(jīng)有了個不錯的理解!
待補充
App 內(nèi)部跳轉除了, 頻繁的控制器之間切換外, 還有比如跳轉到 H5, 或者跳轉到 WebView 等;
App 外跳轉則包含 Scheme 啟動, 3D Touch, UniversalLink, 點擊通知等都會觸發(fā);
這些包含跳轉, 頁面切換的我們均可以統(tǒng)一使用 Router 來進行有效的管理, 使 App 變得更加動態(tài)化, 模塊之間耦合度更低;
- 支持 H5 跳轉
- 外部 Scheme 啟動 App
- UniversalLink
- 3D Touch Shortcut
- 支持后臺動態(tài)下發(fā) RouterMap 配置