http://www.jianshu.com/p/3a902f274a3d
接上一篇移动端路由层设计
为啥要说iOS路由呢?
路由层其实在逻辑上的设计都是一样的,关于对界面跳转的实现部分却与Android平台和iOS平台上的导航机制有着非常紧密的关系,Android操作系统有着天然的架构优势,Intent机制可以协助应用间的交互与通讯,是对调用组件和数据传递的描述,本身这种机制就解除了代码逻辑和界面之间的依赖关系,只有数据依赖。而iOS的界面导航和转场机制则大部分依赖UI组件各自的实现,所以如何解决这个问题,iOS端路由的实现则比较有代表性。
其实说白一点,路由层解决的核心问题就是原来界面或者组件之间相互调用都必须相互依赖,需要导入目标的头文件、需要清楚目标对象的逻辑,而现在全部都通过路由中转,只依赖路由,或者依靠一些消息传递机制连路由都不依赖。其次,路由的核心逻辑就是目标匹配,对于外部调用的情况来说,URL如何匹配Handler是最为重要的,匹配就必然用到正则表达式。了解这些关键点以后就有了设计的目的性,let‘s do it~
这里面有如下几个类:
了解了以上,我们从WLRRouteRequest入手。
其实WLRRouteRequest跟NSURLRequest差不多,不过WLRRouteRequest继承NSObject,实现NSCopying协议,大概如下:
#import
@interface WLRRouteRequest : NSObject<NSCopying>
//外部调用的URL
@property (nonatomic, copy, readonly) NSURL *URL;
//URL表达式,比方说调用登录界面的表达式可以为:AppScheme://user/login/138********,那URL的匹配表达式可以是:/login/:phone([0-9]+),路径必须以/login开头,后面接0-9的电话号码数字,当然你也可以直接把电话号码的正则匹配写全
@property(nonatomic,copy)NSString * routeExpression;
//如果URL是AppScheme://user/login/138********?/callBack="",那么这个callBack就出现在这
@property (nonatomic, copy, readonly) NSDictionary *queryParameters;
//这里面会出现{@"phone":@"138********"}
@property (nonatomic, copy, readonly) NSDictionary *routeParameters;
//这里面存放的是内部调用传递的原生参数
@property (nonatomic, copy, readonly) NSDictionary *primitiveParams;
//自动检测窃取回调的callBack 的Url
@property (nonatomic, strong) NSURL *callbackURL;
//目标的viewcontrolller或者是组件可以通过这个
@property(nonatomic,copy)void(^targetCallBack)(NSError *error,id responseObject);
//用以表明该request是否被消费
@property(nonatomic)BOOL isConsumed;
//简便方法,用以下标法取参数
- (id)objectForKeyedSubscript:(NSString *)key;
//初始化方法
-(instancetype)initWithURL:(NSURL *)URL routeExpression:(NSString *)routeExpression routeParameters:(NSDictionary *)routeParameters primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError * error,id responseObject))targetCallBack;
-(instancetype)initWithURL:(NSURL *)URL;
//默认完成目标的回调
-(void)defaultFinishTargetCallBack;
@end
NSURLRequest其实应该是个值类型的对象,所以实现拷贝协议,该对象的实现部分没有什么可讲的,对照源代码查阅即可。
#import
@class WLRRouteRequest;
@interface WLRRouteHandler : NSObject
//即将handle某一个请求
- (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
//根据request取出调用的目标视图控制器
-(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request;
//根据request取出来源的视图控制器
-(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request;
//开始进行转场
-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;
@end
当WLRRouter对象完成了URL的匹配生成Request,并寻找到Handler的时候,首先会调用- (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
,来确定handler是否愿意处理,如果愿意,则调用-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;
,内部则通过便利方法获取targetViewController与SourceViewController,然后进行转场,核心方法的实现为:
-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error{
UIViewController * sourceViewController = [self sourceViewControllerForTransitionWithRequest:request];
UIViewController * targetViewController = [self targetViewControllerWithRequest:request];
if ((![sourceViewController isKindOfClass:[UIViewController class]])||(![targetViewController isKindOfClass:[UIViewController class]])) {
*error = [NSError WLRTransitionError];
return NO;
}
if (targetViewController != nil) {
targetViewController.wlr_request = request;
}
if ([self preferModalPresentationWithRequest:request]||![sourceViewController isKindOfClass:[UINavigationController class]]) {
[sourceViewController presentViewController:targetViewController animated:YES completion:nil];
}
else if ([sourceViewController isKindOfClass:[UINavigationController class]]){
UINavigationController * nav = (UINavigationController *)sourceViewController;
[nav pushViewController:targetViewController animated:YES];
}
return YES;
}
- (BOOL)preferModalPresentationWithRequest:(WLRRouteRequest *)request;{
return NO;
}
这里根据SourceController的类型进行判断,其实request对象的信息足够可以判断目标视图应该如何打开,从本质上来讲,URL的匹配表达式是跟业务强关联的也是跟UI交互逻辑强关联的,transitionWithRequest方法实现里,你大可以继承一下,然后重写转场过程,甚至你可以在这自己设置iOS7自定义的转场,提供动画控制器和实现转场协议的对象,进而可以整体的控制Appp内部的实现。
该类继承NSRegularExpression
#import
@class WLRMatchResult;
@interface WLRRegularExpression : NSRegularExpression
//传入一个URL返回一个匹配结果
-(WLRMatchResult *)matchResultForString:(NSString *)string;
//根据一个URL的表达式创建一个WLRRegularExpression实例
+(WLRRegularExpression *)expressionWithPattern:(NSString *)pattern;
@end
该对象主要的功能是将一个URL传入查看是否匹配,并且将表达式上声明的路径参数从URL上取下来。
比说,我们设置的URL匹配的表达式是: login/:phone([0-9]+),那AppScheme://user/login/138** 这样的URL应该是匹配,并且将138的手机号取出来,对应到phone上,这个过程必须用到正则表达式的分组提取子串的功能,:phone是约定好的提取子串的值对应的key的名字,其实这个url的正则表达式应该是: /login/([0-9]+)$,那么WLRRegularExpression对象需要知道需要提取所有子串的key还有将URL匹配的表达式转换为真正的正则表达式。
-(instancetype)initWithPattern:(NSString *)pattern options:(NSRegularExpressionOptions)options error:(NSError * _Nullable __autoreleasing *)error{
//初始化方法中将URL匹配的表达式pattern转换为真正的正则表达式
NSString *transformedPattern = [WLRRegularExpression transfromFromPattern:pattern];
//用转化后的结果初始化父类
if (self = [super initWithPattern:transformedPattern options:options error:error]) {
//同时将需要提取的子串的值的Key保存到数组中
self.routerParamNamesArr = [[self class] routeParamNamesFromPattern:pattern];
}
return self;
}
//转换为正则表达式
+(NSString*)transfromFromPattern:(NSString *)pattern{
//将pattern拷贝
NSString * transfromedPattern = [NSString stringWithString:pattern];
//利用:[a-zA-Z0-9-_][^/]+这个正则表达式,将URL匹配的表达式的子串key提取出来,也就是像 /login/:phone([0-9]+)/:name[a-zA-Z-_]这样的pattern,需要将:phone([0-9]+)和:name[a-zA-Z-_]提取出来
NSArray * paramPatternStrings = [self paramPatternStringsFromPattern:pattern];
NSError * err;
//再根据:[a-zA-Z0-9-_]+这个正则表达式,将带有提取子串的key全部去除,比如将:phone([0-9]+)去除:phone改成([0-9]+)
NSRegularExpression * paramNamePatternEx = [NSRegularExpression regularExpressionWithPattern:WLRRouteParamNamePattern options:NSRegularExpressionCaseInsensitive error:&err];
for (NSString * paramPatternString in paramPatternStrings) {
NSString * replaceParamPatternString = [paramPatternString copy];
NSTextCheckingResult * foundParamNamePatternResult =[paramNamePatternEx matchesInString:paramPatternString options:NSMatchingReportProgress range:NSMakeRange(0, paramPatternString.length)].firstObject;
if (foundParamNamePatternResult) {
NSString *paramNamePatternString =[paramPatternString substringWithRange: foundParamNamePatternResult.range];
replaceParamPatternString = [replaceParamPatternString stringByReplacingOccurrencesOfString:paramNamePatternString withString:@""];
}
if (replaceParamPatternString.length == 0) {
replaceParamPatternString = WLPRouteParamMatchPattern;
}
transfromedPattern = [transfromedPattern stringByReplacingOccurrencesOfString:paramPatternString withString:replaceParamPatternString];
}
if (transfromedPattern.length && !([transfromedPattern characterAtIndex:0] == '/')) {
transfromedPattern = [@"^" stringByAppendingString:transfromedPattern];
}
//最后结尾要用$符号
transfromedPattern = [transfromedPattern stringByAppendingString:@"$"];
//最后会将/login/:phone([0-9]+)转换为login/([0-9]+)$
return transfromedPattern;
}
在Matcher对象匹配一个URL的时候
-(WLRMatchResult *)matchResultForString:(NSString *)string{
//首先通过自身方法将URL进行匹配得出NSTextCheckingResult结果的数组
NSArray * array = [self matchesInString:string options:0 range:NSMakeRange(0, string.length)];
WLRMatchResult * result = [[WLRMatchResult alloc]init];
if (array.count == 0) {
return result;
}
result.match = YES;
NSMutableDictionary * paramDict = [NSMutableDictionary dictionary];
//遍历NSTextCheckingResult结果
for (NSTextCheckingResult * paramResult in array) {
//再便利根据初始化的时候提取的子串的Key的数组
for (int i = 1; i.numberOfRanges&&i <= self.routerParamNamesArr.count;i++ ) {
NSString * paramName = self.routerParamNamesArr[i-1];
//将值取出,然后将key和value放入到paramDict
NSString * paramValue = [string substringWithRange:[paramResult rangeAtIndex:i]];
[paramDict setObject:paramValue forKey:paramName];
}
}
//最后赋值给WLRMatchResult对象
result.paramProperties = paramDict;
return result;
}
核心代码总共80多行,源码大家可以详阅
#import
@class WLRRouteRequest;
@interface WLRRouteMatcher : NSObject
//传入URL匹配的表达式,获取一个matcher实例
+(instancetype)matcherWithRouteExpression:(NSString *)expression;
//传入URL,如果能匹配上,则生成WLRRouteRequest对象,同时将各种参数解析好交由WLRRouteRequest携带
-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack;
@end
属性有如下:
//scheme
@property(nonatomic,copy) NSString * scheme;
//WLRRegularExpression的实例
@property(nonatomic,strong)WLRRegularExpression * regexMatcher;
//匹配的表达式
@property(nonatomic,copy)NSString * routeExpressionPattern;
初始化方法:
-(instancetype)initWithRouteExpression:(NSString *)routeExpression{
if (![routeExpression length]) {
return nil;
}
if (self = [super init]) {
//将scheme与path部分分别取出
NSArray * parts = [routeExpression componentsSeparatedByString:@"://"];
_scheme = parts.count>1?[parts firstObject]:nil;
_routeExpressionPattern =[parts lastObject];
//将path部分当做URL匹配表达式生成WLRRegularExpression实例
_regexMatcher = [WLRRegularExpression expressionWithPattern:_routeExpressionPattern];
}
return self;
}
匹配方法:
-(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void (^)(NSError *, id))targetCallBack{
NSString * urlString = [NSString stringWithFormat:@"%@%@",URL.host,URL.path];
if (self.scheme.length && ![self.scheme isEqualToString:URL.scheme]) {
return nil;
}
//调用self.regexMatcher将URL传入,获取WLRMatchResult结果,看是否匹配
WLRMatchResult * result = [self.regexMatcher matchResultForString:urlString];
if (!result.isMatch) {
return nil;
}
//如果匹配,则将result.paramProperties路径参数传入,初始化一个WLRRouteRequest实例
WLRRouteRequest * request = [[WLRRouteRequest alloc]initWithURL:URL routeExpression:self.routeExpressionPattern routeParameters:result.paramProperties primitiveParameters:primitiveParameters targetCallBack:targetCallBack];
return request;
}
@class WLRRouteRequest;
@class WLRRouteHandler;
@interface WLRRouter : NSObject
//注册block回调的URL匹配表达式,可用作内部调用
-(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest * request))routeHandlerBlock forRoute:(NSString *)route;
//注册一个WLRRouteHandler对应的URL匹配表达式route
-(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route;
//判断url是否可以被handle
-(BOOL)canHandleWithURL:(NSURL *)url;
-(void)setObject:(id)obj forKeyedSubscript:(NSString *)key;
-(id)objectForKeyedSubscript:(NSString *)key;
//调用handleURL方法,传入URL、原生参数和targetCallBack和完成匹配的completionBlock
-(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock;
在实现部分,有三个属性:
//每一个URL的匹配表达式route对应一个matcher实例,放在字典中
@property(nonatomic,strong)NSMutableDictionary * routeMatchers;
//每一个URL匹配表达式route对应一个WLRRouteHandler实例
@property(nonatomic,strong)NSMutableDictionary * routeHandles;
//每一个URL匹配表达式route对应一个回调的block
@property(nonatomic,strong)NSMutableDictionary * routeblocks;
在Route挂在Handler和回调的block的时候:
-(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest *))routeHandlerBlock forRoute:(NSString *)route{
if (routeHandlerBlock && [route length]) {
//首先添加一个WLRRouteMatcher实例
[self.routeMatchers setObject:[WLRRouteMatcher matcherWithRouteExpression:route] forKey:route];
//删除route对应的handler对象
[self.routeHandles removeObjectForKey:route];
//将routeHandlerBlock和route存入对应关系的字典中
self.routeblocks[route] = routeHandlerBlock;
}
}
-(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route{
if (handler && [route length]) {
//首先生成route对应的WLRRouteMatcher实例
[self.routeMatchers setObject:[WLRRouteMatcher matcherWithRouteExpression:route] forKey:route];
//删除route对应的block回调
[self.routeblocks removeObjectForKey:route];
//设置route对应的handler
self.routeHandles[route] = handler;
}
}
接下来完善handle方法:
-(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *error, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock{
if (!URL) {
return NO;
}
NSError * error;
WLRRouteRequest * request;
__block BOOL isHandled = NO;
//遍历routeMatchers中的WLRRouteMatcher对象,将URL传入对象,看是否能得到WLRRouteRequest对象
for (NSString * route in self.routeMatchers.allKeys) {
WLRRouteMatcher * matcher = [self.routeMatchers objectForKey:route];
WLRRouteRequest * request = [matcher createRequestWithURL:URL primitiveParameters:primitiveParameters targetCallBack:targetCallBack];
if (request) {
//如果得到WLRRouteRequest对象,说明匹配成功,则进行handler的生命周期函数调用或是这block回调
isHandled = [self handleRouteExpression:route withRequest:request error:&error];
break;
}
}
if (!request) {
error = [NSError WLRNotFoundError];
}
//在调用完毕block或者是handler的生命周期方法以后,回调完成的completionHandler
[self completeRouteWithSuccess:isHandled error:error completionHandler:completionBlock];
return isHandled;
}
//根据request进行handler的生命周期函数调用或者是block回调
-(BOOL)handleRouteExpression:(NSString *)routeExpression withRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error {
id handler = self[routeExpression];
//self.routeHandles和self.routeblocks拿到route对应的回调block或者是handler实例
if ([handler isKindOfClass:NSClassFromString(@"NSBlock")]) {
WLRRouteRequest *(^blcok)(WLRRouteRequest *) = handler;
//调用回调的block
WLRRouteRequest * backRequest = blcok(request);
//判断block里面是否消费了此request,如果没有而目标设置了目标回调targetCallBack,那么在此进行默认回调
if (backRequest.isConsumed==NO) {
if (backRequest.targetCallBack) {
dispatch_async(dispatch_get_main_queue(), ^{
backRequest.targetCallBack(nil,nil);
});
}
}
return YES;
}
else if ([handler isKindOfClass:[WLRRouteHandler class]]){
//拿到url对应的handler对象后,先调用handler的shouldHandleWithRequest方法,如果返回YES,则调用进行转场的transitionWithRequest方法
WLRRouteHandler * rHandler = (WLRRouteHandler *)handler;
if (![rHandler shouldHandleWithRequest:request]) {
return NO;
}
return [rHandler transitionWithRequest:request error:error];
}
return YES;
}
以上我们可以看到,Route将匹配的逻辑单独封装到WLRRouteMatcher对象中,将匹配后的结果生成WLRRouteRequest实例以携带足够完整的数据,同时将真正处理视图控制器的转场或者是组件的加载或者是未来可能拓展的handle业务封装到WLRRouteHandler实例中,匹配逻辑对应的处理逻辑干净分离,匹配逻辑可单独塑造业务匹配,处理逻辑可以通过继承扩展或者冲洗WLRRouteHandler的生命周期函数来更好的处理回调业务。如果WLRRouteHandler不能提供足够多的扩展性,则可以使用block回调最大限度的进行扩展。
以上,就是路由部分的整体实现。
在WLRRouteHandler中,其实我们可以单独控制路由经过的页面跳转的转场。
-(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request{
}
-(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request{
}
-(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error{
}
这样的生命周期函数是不是很像UIViewControllerContextTransitioning转场上下文的协议的设定?- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
方法使上下文提供目标控制器和源控制器,其实在handler中你完全可以自定义一个子类,在transitionWithRequest方法里,设置遵守UIViewControllerTransitioningDelegate的代理,然后在此提供遵守 UIViewControllerAnimatedTransitioning的动画控制器,然后自定义转场上下文,实现自定义UI转场,而对应的匹配逻辑是与此无关的,我们就可以在路由曾控制全局的页面转场效果。对自定义转场不太熟悉的同学请移步我之前的文章:
ContainerViewController的ViewController 转场
有两个方面可以去做
-(BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request
中可以检测request中的参数,比方说效验source或者是效验业务参数完整等最后附上代码地址:
喜欢的来个星吧…
https://github.com/Neojoke/WLRRoute