一步步擼一個app模組化路由、URLscheme訪問
為什麼要做路由
這個問題就要提到app 開發模組化的思想了,試想一下你的app是一個電商專案,那麼你的產品詳情頁、列表頁、購物車、搜尋等頁面肯定就是呼叫頻次非常高的VC了,這些介面之間跳轉都會非常頻繁。這就造成了互相依賴並且高度耦合。如下圖:

image
是不是非常的亂?相互之間都有依賴,在列表需要import詳情頁面,詳情頁面也要import列表,購物車等等。你有可能說,這沒什麼啊,絲毫不影響開發效率。但是當一個app在公司業務發展的過程中體積越來越龐大,其中堆疊了大量的業務邏輯程式碼,不同業務的程式碼相互呼叫,相互巢狀,程式碼之間的耦合性越來越高,呼叫邏輯會越來越混亂,程式碼看來起就很糟糕了,將來的升級就需要對程式碼進行大量修改及調整,帶來的工作量是非常巨大的。一個良好的開始,非常的重要。模組之間要相互獨立,如果你的程式碼vc之間的import很多,那就說明耦合度很高了。模組化可以將程式碼的功能邏輯儘量封裝在一起,對外只提供介面,業務邏輯程式碼與功能模組通過介面進行弱耦合。這就需要設計一套符合要求的元件之間通訊的中介軟體,這個中介軟體就稱為路由。

image
下面我們開始擼程式碼
app內部訪問
建立route管理類
根據上圖很簡單的就想到了一個方法,提供一箇中間層:Router。在router裡面定義好每次跳轉的方法,然後在需要用的介面呼叫router函式,傳入對應的引數。比如這樣:
#import "GoodsDetailViewController.h" #import "ShopCartViewController.h" @implementation Router + (UIViewController *) getDetailWithParam:(NSString *) param { GoodsDetailViewController *detailVC = [[GoodsDetailViewController alloc]initWithProId:self.proId]; return detailVC; } + (UIViewController *) getCart { ShopCartViewController *cartVC = [[ShopCartViewController alloc] init]; return cartVC; } @end
在其他頁面這樣使用
#import "Router.m" UIViewController * detailVC = [[Router instance] getDetailWithParam:param]; [self.navigationController pushViewController: detailVC];
但是這樣做也有一個問題,Router裡面會依賴所有的VC。那如何打破這層迴圈引用呢?
利用OC runtime的特性 動態初始化物件 打破import魔咒
程式碼
UIViewController * _Nonnull y_controller(NSString *name){ if (!name||name.length==0) { LogError(@"請傳入class名"); return nil; } id vc = [[NSClassFromString(name) alloc] init]; if (vc) { if ([vc isKindOfClass:[UIViewController class]]) { return vc; } NSString *error = [NSString stringWithFormat:@"Class %@不是controller",name]; LogError(error); return nil; }else{ NSString *error = [NSString stringWithFormat:@"Class %@不存在",name]; LogError(error); return nil; } }
這樣我們就能動態獲取到controller了,那麼如果傳遞引數呢,每個模組定製的對外介面引數肯定不一樣,同樣的我們可以用kvc的形式進行動態引數傳遞
//呼叫某個頁面 - (BOOL)pushVcName:(NSString *)vcName from:(UIViewController *)fromvc withData:(NSDictionary *)data{ UIViewController *vc = y_controller(vcName); return [self push:vc from:fromvc withData:data]; } - (BOOL)push:(UIViewController *)vc from:(UIViewController *)fromvcwithData:(NSDictionary *)data;{ UIViewController *a = fromvc?fromvc:y_currentController(); if (!vc) { return NO; } //根據字典進行屬性賦值 [data enumerateKeysAndObjectsUsingBlock:^(id_Nonnull key, id_Nonnull obj, BOOL * _Nonnull stop) { @try { [vc setValue:obj forKey:key]; } @catch (NSException *exception) { } @finally { } }]; [a.navigationController pushViewController:vc animated:YES]; return YES; }
進行到這裡,你可能想問,我們回撥怎麼處理,vc業務之間肯定少不了回撥。
這裡我想到的一種解決辦法是,同樣的可以把block放到data字典引數裡面:
id callback = ^(NSString *pass){ NSLog(@"%@", pass); }; //callback 目標頁面有這個block屬性即可 @{"name":@"sdsaad",@"callBack":callback}
外部呼叫示例
id call = ^(NSString *aa){ NSLog(@"%@",aa); }; [[YINRouteManager shareInstance] pushVcName:@"LoginViewController" from:self withData:@{@"callBack":call,@"name":@"test"}];
如果覺得模組之間用類名進行訪問太複雜了(因為oc是無名稱空間的,類名通常都比較的長,而且用類名進行通訊比較的敏感),或者是在專案開發中,對應的模組還沒開發出來,我們要進行訪問。怎麼辦呢?我們可以先定義模組標識。比如
//login是定義的登陸模組 對應LoginViewController [[YINRouteManager shareInstance] pushVcName:@"login" from:self withData:@{@"callBack":call,@"name":@"test"}];
模組註冊路由標識
@implementation LoginViewController + (void)load{ [self y_registPath:@"login"]; }
@implementation UIViewController (YINRoute) + (void)y_registPath:(NSString *)appOpenPath{ if (!appOpenPath||appOpenPath.length<1) { [[YINRouteURLPathRegist shareInstance] removePathRegist:NSStringFromClass(self.class)]; }else{ [[YINRouteURLPathRegist shareInstance] registClass:NSStringFromClass(self.class) withPath:appOpenPath]; } }
@implementation YINRouteURLPathRegist - (void)registClass:(NSString *)className withPath:(NSString *)path;{ if (!className||className.length==0) { return; } if (!path||path.length==0) { [self removePathRegist:className]; return; } [self.registDict setObject:className forKey:path]; } - (void)removePathRegist:(NSString *)className;{ if (!className||className.length==0) { return; } __block NSString *removeKey = nil; [self.registDict enumerateKeysAndObjectsUsingBlock:^(id_Nonnull key, id_Nonnull obj, BOOL * _Nonnull stop) { if ([obj isEqualToString:className]) { removeKey = key; *stop = YES; } }]; if (removeKey) { [self.registDict removeObjectForKey:removeKey]; } }
標示註冊記錄好之後,在獲取controller的方法裡面要作一點調整
UIViewController * _Nonnull y_controller(NSString *name){ if (!name||name.length==0) { LogError(@"請傳入class名"); return nil; } //如果此路由標示有對應的類名 註冊 if ([[YINRouteURLPathRegist shareInstance].registDict objectForKey:name]) { //取出真實的類名 name = [[YINRouteURLPathRegist shareInstance].registDict objectForKey:name]; } id vc = [[NSClassFromString(name) alloc] init]; if (vc) { if ([vc isKindOfClass:[UIViewController class]]) { return vc; } NSString *error = [NSString stringWithFormat:@"Class %@不是controller",name]; LogError(error); return nil; }else{ NSString *error = [NSString stringWithFormat:@"Class %@不存在",name]; LogError(error); return nil; } }
每次使用route進行訪問太麻煩,建立一個controller的分類
@implementation UIViewController (YINRoute) + (void)y_registPath:(NSString *)appOpenPath{ if (!appOpenPath||appOpenPath.length<1) { [[YINRouteURLPathRegist shareInstance] removePathRegist:NSStringFromClass(self.class)]; }else{ [[YINRouteURLPathRegist shareInstance] registClass:NSStringFromClass(self.class) withPath:appOpenPath]; } } - (BOOL)y_push:(UIViewController *)vc;{ return [self y_push:vc withData:nil]; } - (BOOL)y_present:(UIViewController *)vc;{ return [self y_present:vc withData:nil]; } - (BOOL)y_push:(UIViewController *)vc withData:(NSDictionary *)data;{ return [[YINRouteManager shareInstance] push:vc from:self withData:data]; } - (BOOL)y_present:(UIViewController *)vc withData:(NSDictionary *)data;{ return [[YINRouteManager shareInstance] present:vc from:self withData:data]; } - (BOOL)y_pushVcName:(NSString *)vcName;{ return [self y_pushVcName:vcName withData:nil]; } - (BOOL)y_presentVcName:(NSString *)vcName;{ return [self y_presentVcName:vcName withData:nil]; } - (BOOL)y_pushVcName:(NSString *)vcName withData:(NSDictionary *)data;{ return [[YINRouteManager shareInstance] pushVcName:vcName from:self withData:data]; } - (BOOL)y_presentVcName:(NSString *)vcName withData:(NSDictionary *)data;{ return [[YINRouteManager shareInstance] presentVcName:vcName from:self withData:data]; }
這樣一個簡單的app內部路由器就成型了。
App之間的訪問 擼一個外部路由
那麼app之間的跳轉有什麼作用呢?我們所使用的每一個app就相當於一個功能,也是一個模組。app的跳轉可以使得每個app就像一個功能元件一樣,幫助我們完成需要做的事情,比如三方支付,搜尋,導航,分享等等。
要跳轉到別人的app,就要知道別人的app的跳轉協議是什麼,需要傳入什麼引數,我們常見的跳轉協議有下面這些:
1.開啟Mail [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"mailto://[email protected]"]] 2.開啟電話 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"tel://18688886666"]]; 3.開啟SMS [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"sms:18688886666"]];
註冊一個URL Scheme

螢幕快照 2018-10-27 16.21.24.png
這樣其他app就可以通過此urlschemes來訪問了
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"BLBaseAPP://"]];
制定協議
app間的訪問有兩種情況1.開啟某個頁面2.執行某個函式。其實也只可以理解為執行某個函式。兩種情況只是細分
建議:
在host位傳入通訊型別 (page或action) path位如果是page型別就傳入頁面標識 action型別就傳入方法標識
//NSURL *url = [NSURL URLWithString:@" ofollow,noindex">urlscheme://host/path?page=100 "];
建立YINRouteURLData用於接受處理URL
typedef NS_ENUM(NSInteger, YINAppRouteType) { YINAppRouteTypePage = 0, // 開啟頁面 YINAppRouteTypeAction = 1, // 呼叫方法 }; @implementation YINRouteURLData + (instancetype)urlDataWithUrl:(NSURL *)url;{ YINRouteURLData *a =[[self alloc] init]; a.URL = url; return a; } - (NSURL *)URL{ if (!_URL) { _URL = [NSURL URLWithString:@"app://"]; } return _URL; } //如果是頁面訪問型別,獲取頁面標示 - (NSString *)controllerName{ if (self.routeType == YINAppRouteTypePage) { NSString *path =self.URL.path.length>0?[self.URL.path substringFromIndex:1]:@""; return path; } return nil; } /** 路由型別YINAppRouteTypePage = 0, // 開啟頁面 YINAppRouteTypeAction = 1, // 呼叫方法 */ - (YINAppRouteType)routeType{ if (self.URL.host&&[self.URL.host isEqualToString:[YINRouteConfig actionHost]]) { return YINAppRouteTypeAction; } return YINAppRouteTypePage; } //執行的方法標示 - (NSString *)actionName{ if (self.routeType == YINAppRouteTypeAction) { return self.URL.path.length>0?[self.URL.path substringFromIndex:1]:@""; } return nil; } //url引數解析為字典 - (NSDictionary *)data{ NSString *dataStr = [NSString stringWithFormat:@"%@",self.URL.query]; NSArray *keyValues = [dataStr componentsSeparatedByString:@"&"]; NSMutableDictionary *dic = @{}.mutableCopy; [keyValues enumerateObjectsUsingBlock:^(id_Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { [dic setObject:[obj componentsSeparatedByString:@"="].lastObject forKey:[obj componentsSeparatedByString:@"="].firstObject]; }]; return dic; }
appdelegate接受到url訪問
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{ [[YINRouteManager shareInstance] appActionWithUrl:url]; return YES; }
YINRouteManager內部處理
//處理app間的通訊、跳轉等事件 - (BOOL)appActionWithUrl:(NSURL *)url{ if (![[YINRouteConfig urlScheme] containsObject:url.scheme]) { return NO; } YINRouteURLData *data = [YINRouteURLData urlDataWithUrl:url]; if (data.routeType==YINAppRouteTypePage) { //頁面跳轉型別 return [self pushVcName:data.controllerName from:nil withData:data.data]; }else{ #warning 請在此處根據需求實現邏輯 //方法執行 //NSString *actionName = data.actionName; //NSString *actionData = data.data; } return YES; }
url訪問控制的配置
@interface YINRouteConfig : NSObject //判斷通訊型別為方法執行 @property (copy,nonatomic) NSString *actionHost; //判斷通訊型別為頁面跳轉 @property (strong,nonatomic) NSString *openPageHost; //url通訊時對滿足此條件的urlScheme進行路由控制 為什麼使用陣列呢 因為有可能對不同的介入app開放不同的urlscheme 一般情況只判斷一種 @property (copy,nonatomic) NSArray <NSString *> * urlScheme; + (instancetype)shareInstance; @end
開啟URL訪問功能,並配置規則
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [YINRouteManager startWithUrlSchemes:@[@"YINRouteDemo"] pageHost:@"open" actionHost:@"action" actionBlock:^(NSString *actionName, id data) { NSLog(@"執行方法%@",actionName); NSLog(@"引數%@",data); }]; return YES; }
這樣我們就能通過URL的形式訪問app所有的頁面和執行一些特定方法
//訪問頁面 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"YINRouteDemo://open/LoginViewController?name=123213&pass=123"]]; //執行方法 [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"YINRouteDemo://action/logPrint?name=123213&pass=123"]];
內部路由和外部路由規則統一
通過上面的程式碼,已經做到了規則統一。這樣我們也可以用這樣的程式碼訪問app內的任何模組了
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"YINRouteDemo://open/LoginViewController?name=123213&pass=123"]];
如果在瀏覽器或者其他app訪問這個連結,則會開啟app然後進入登陸頁面並傳遞引數。
當然我們如果給LoginViewController註冊了路由標示為login
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"YINRouteDemo://open/login?name=123213&pass=123"]];
也是一樣的效果
如果是在app內訪問,直接跳轉到登陸頁面。
app內建議還是用controller的分類進行路由就好了。
總結
YINRoute
app模組化路由管理器,app間urlscheme訪問管理器
呼叫示例
id call = ^(NSString *aa){ NSLog(@"%@",aa); }; [[YINRouteManager shareInstance] pushVcName:@"LoginViewController" from:self withData:@{@"callBack":call,@"name":@"test"}];
設定特殊路由標示
@implementation LoginViewController + (void)load{ //設定了路由標示後 既可以通過類名訪問 也可以通過標示訪問 [self y_registPath:@"login"]; }
controller分類方法快捷呼叫
//頁面跳轉 [self y_pushVcName:@"LoginViewController" withData:@{ @"name":@"12323121" }]; [self y_pushVcName:@"login" withData:@{ @"name":@"12323121" }];
url形式訪問模組 此方法同時支援app 內模組間訪問 也支援app之間的訪問
開啟url訪問功能 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [YINRouteManager startWithUrlSchemes:@[@"YINRouteDemo"] pageHost:@"open" actionHost:@"action" actionBlock:^(NSString *actionName, id data) { NSLog(@"執行方法%@",actionName); NSLog(@"引數%@",data); }]; return YES; }
訪問頁面
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"BLBaseAPP://open/LoginViewController?name=123213&pass=123"]];
執行方法
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"BLBaseAPP://action/logPrint?name=123213&pass=123"]];
整合
pod "YINRoute"