//
|
// DHRouter.m
|
// Pods
|
//
|
// Created by Anson on 2017/5/18.
|
//
|
//
|
|
#import "DHRouter.h"
|
|
|
NSString *const DHRouterParameterURL = @"MGJRouterParameterURL";
|
NSString *const DHRouterParameterCompletion = @"MGJRouterParameterCompletion";
|
NSString *const DHRouterParameterUserInfo = @"MGJRouterParameterUserInfo";
|
|
static NSString * const MGJ_ROUTER_WILDCARD_CHARACTER = @"~";
|
static NSString *specialCharacters = @"/?&.";
|
|
NSString *const MGJRouterParameterURL = @"MGJRouterParameterURL";
|
NSString *const MGJRouterParameterCompletion = @"MGJRouterParameterCompletion";
|
NSString *const MGJRouterParameterUserInfo = @"MGJRouterParameterUserInfo";
|
|
/**
|
* routerParameters 里内置的几个参数会用到上面定义的 string
|
*/
|
typedef void (^MGJRouterHandler)(NSDictionary *routerParameters);
|
|
/**
|
* 需要返回一个 object,配合 objectForURL: 使用
|
*/
|
typedef id (^MGJRouterObjectHandler)(NSDictionary *routerParameters);
|
|
@interface MGJRouter : NSObject
|
|
/**
|
* 注册 URLPattern 对应的 Handler,在 handler 中可以初始化 VC,然后对 VC 做各种操作
|
*
|
* @param URLPattern 带上 scheme,如 mgj://beauty/:id
|
* @param handler 该 block 会传一个字典,包含了注册的 URL 中对应的变量。
|
* 假如注册的 URL 为 mgj://beauty/:id 那么,就会传一个 @{@"id": 4} 这样的字典过来
|
*/
|
+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(MGJRouterHandler)handler;
|
|
/**
|
* 注册 URLPattern 对应的 ObjectHandler,需要返回一个 object 给调用方
|
*
|
* @param URLPattern 带上 scheme,如 mgj://beauty/:id
|
* @param handler 该 block 会传一个字典,包含了注册的 URL 中对应的变量。
|
* 假如注册的 URL 为 mgj://beauty/:id 那么,就会传一个 @{@"id": 4} 这样的字典过来
|
* 自带的 key 为 @"url" 和 @"completion" (如果有的话)
|
*/
|
+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler;
|
|
/**
|
* 取消注册某个 URL Pattern
|
*
|
* @param URLPattern 已注册的路由url
|
*/
|
+ (void)deregisterURLPattern:(NSString *)URLPattern;
|
|
/**
|
* 打开此 URL
|
* 会在已注册的 URL -> Handler 中寻找,如果找到,则执行 Handler
|
*
|
* @param URL 带 Scheme,如 mgj://beauty/3
|
*/
|
+ (void)openURL:(NSString *)URL;
|
|
/**
|
* 打开此 URL,同时当操作完成时,执行额外的代码
|
*
|
* @param URL 带 Scheme 的 URL,如 mgj://beauty/4
|
* @param completion URL 处理完成后的 callback,完成的判定跟具体的业务相关
|
*/
|
+ (void)openURL:(NSString *)URL completion:(void (^)(id result))completion;
|
|
/**
|
* 打开此 URL,带上附加信息,同时当操作完成时,执行额外的代码
|
*
|
* @param URL 带 Scheme 的 URL,如 mgj://beauty/4
|
* @param userInfo 附加参数
|
* @param completion URL 处理完成后的 callback,完成的判定跟具体的业务相关
|
*/
|
+ (void)openURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo completion:(void (^)(id result))completion;
|
|
/**
|
* 查找谁对某个 URL 感兴趣,如果有的话,返回一个 object
|
*
|
* @param URL 已注册的路由url
|
*/
|
+ (id)objectForURL:(NSString *)URL;
|
|
/**
|
* 查找谁对某个 URL 感兴趣,如果有的话,返回一个 object
|
*
|
* @param URL 已注册的路由url
|
* @param userInfo 参数
|
*/
|
+ (id)objectForURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo;
|
|
/**
|
* 是否可以打开URL
|
*
|
* @param URL 路由url
|
*
|
* @return YES or NO
|
*/
|
+ (BOOL)canOpenURL:(NSString *)URL;
|
|
/**
|
* 调用此方法来拼接 urlpattern 和 parameters
|
*
|
* #define MGJ_ROUTE_BEAUTY @"beauty/:id"
|
* [MGJRouter generateURLWithPattern:MGJ_ROUTE_BEAUTY, @[@13]];
|
*
|
*
|
* @param pattern url pattern 比如 @"beauty/:id"
|
* @param parameters 一个数组,数量要跟 pattern 里的变量一致
|
*
|
* @return Route url
|
*/
|
+ (NSString *)generateURLWithPattern:(NSString *)pattern parameters:(NSArray *)parameters;
|
@end
|
|
@interface MGJRouter ()
|
/**
|
* 保存了所有已注册的 URL
|
* 结构类似 @{@"beauty": @{@":id": {@"_", [block copy]}}}
|
*/
|
@property (nonatomic) NSMutableDictionary *routes;
|
@end
|
|
@implementation MGJRouter
|
|
+ (instancetype)sharedInstance
|
{
|
static MGJRouter *instance = nil;
|
static dispatch_once_t onceToken;
|
dispatch_once(&onceToken, ^{
|
instance = [[self alloc] init];
|
});
|
return instance;
|
}
|
|
+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(MGJRouterHandler)handler
|
{
|
[[self sharedInstance] addURLPattern:URLPattern andHandler:handler];
|
}
|
|
+ (void)deregisterURLPattern:(NSString *)URLPattern
|
{
|
[[self sharedInstance] removeURLPattern:URLPattern];
|
}
|
|
+ (void)openURL:(NSString *)URL
|
{
|
[self openURL:URL completion:nil];
|
}
|
|
+ (void)openURL:(NSString *)URL completion:(void (^)(id result))completion
|
{
|
[self openURL:URL withUserInfo:nil completion:completion];
|
}
|
|
+ (void)openURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo completion:(void (^)(id result))completion
|
{
|
URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
NSMutableDictionary *parameters = [[self sharedInstance] extractParametersFromURL:URL];
|
|
[parameters enumerateKeysAndObjectsUsingBlock:^(id key, NSString *obj, BOOL *stop) {
|
if ([obj isKindOfClass:[NSString class]]) {
|
parameters[key] = [obj stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
}
|
}];
|
|
if (parameters) {
|
MGJRouterHandler handler = parameters[@"block"];
|
if (completion) {
|
parameters[MGJRouterParameterCompletion] = completion;
|
}
|
if (userInfo) {
|
parameters[MGJRouterParameterUserInfo] = userInfo;
|
}
|
if (handler) {
|
[parameters removeObjectForKey:@"block"];
|
handler(parameters);
|
}
|
}
|
}
|
|
+ (BOOL)canOpenURL:(NSString *)URL
|
{
|
return [[self sharedInstance] extractParametersFromURL:URL] ? YES : NO;
|
}
|
|
+ (NSString *)generateURLWithPattern:(NSString *)pattern parameters:(NSArray *)parameters
|
{
|
NSInteger startIndexOfColon = 0;
|
|
NSMutableArray *placeholders = [NSMutableArray array];
|
|
for (int i = 0; i < pattern.length; i++) {
|
NSString *character = [NSString stringWithFormat:@"%c", [pattern characterAtIndex:i]];
|
if ([character isEqualToString:@":"]) {
|
startIndexOfColon = i;
|
}
|
if ([specialCharacters rangeOfString:character].location != NSNotFound && i > (startIndexOfColon + 1) && startIndexOfColon) {
|
NSRange range = NSMakeRange(startIndexOfColon, i - startIndexOfColon);
|
NSString *placeholder = [pattern substringWithRange:range];
|
if (![self checkIfContainsSpecialCharacter:placeholder]) {
|
[placeholders addObject:placeholder];
|
startIndexOfColon = 0;
|
}
|
}
|
if (i == pattern.length - 1 && startIndexOfColon) {
|
NSRange range = NSMakeRange(startIndexOfColon, i - startIndexOfColon + 1);
|
NSString *placeholder = [pattern substringWithRange:range];
|
if (![self checkIfContainsSpecialCharacter:placeholder]) {
|
[placeholders addObject:placeholder];
|
}
|
}
|
}
|
|
__block NSString *parsedResult = pattern;
|
|
[placeholders enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL * _Nonnull stop) {
|
idx = parameters.count > idx ? idx : parameters.count - 1;
|
parsedResult = [parsedResult stringByReplacingOccurrencesOfString:obj withString:parameters[idx]];
|
}];
|
|
return parsedResult;
|
}
|
|
+ (id)objectForURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo
|
{
|
MGJRouter *router = [MGJRouter sharedInstance];
|
|
URL = [URL stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
|
NSMutableDictionary *parameters = [router extractParametersFromURL:URL];
|
MGJRouterObjectHandler handler = parameters[@"block"];
|
|
if (handler) {
|
if (userInfo) {
|
parameters[MGJRouterParameterUserInfo] = userInfo;
|
}
|
[parameters removeObjectForKey:@"block"];
|
return handler(parameters);
|
}
|
return nil;
|
}
|
|
+ (id)objectForURL:(NSString *)URL
|
{
|
return [self objectForURL:URL withUserInfo:nil];
|
}
|
|
+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(MGJRouterObjectHandler)handler
|
{
|
[[self sharedInstance] addURLPattern:URLPattern andObjectHandler:handler];
|
}
|
|
- (void)addURLPattern:(NSString *)URLPattern andHandler:(MGJRouterHandler)handler
|
{
|
NSMutableDictionary *subRoutes = [self addURLPattern:URLPattern];
|
if (handler && subRoutes) {
|
subRoutes[@"_"] = [handler copy];
|
}
|
}
|
|
- (void)addURLPattern:(NSString *)URLPattern andObjectHandler:(MGJRouterObjectHandler)handler
|
{
|
NSMutableDictionary *subRoutes = [self addURLPattern:URLPattern];
|
if (handler && subRoutes) {
|
subRoutes[@"_"] = [handler copy];
|
}
|
}
|
|
- (NSMutableDictionary *)addURLPattern:(NSString *)URLPattern
|
{
|
//取出URL中所有路径
|
NSArray *pathComponents = [self pathComponentsFromURL:URLPattern];
|
|
NSInteger index = 0;
|
NSMutableDictionary* subRoutes = self.routes;
|
|
//遍历所有路径
|
while (index < pathComponents.count)
|
{
|
NSString* pathComponent = pathComponents[index];
|
if (![subRoutes objectForKey:pathComponent])
|
{
|
subRoutes[pathComponent] = [[NSMutableDictionary alloc] init];
|
}
|
subRoutes = subRoutes[pathComponent];
|
index++;
|
}
|
return subRoutes;
|
}
|
|
#pragma mark - Utils
|
|
- (NSMutableDictionary *)extractParametersFromURL:(NSString *)url
|
{
|
NSMutableDictionary* parameters = [NSMutableDictionary dictionary];
|
|
parameters[MGJRouterParameterURL] = url;
|
|
NSMutableDictionary* subRoutes = self.routes;
|
NSArray* pathComponents = [self pathComponentsFromURL:url];
|
|
BOOL found = NO;
|
// borrowed from HHRouter(https://github.com/Huohua/HHRouter)
|
for (NSString* pathComponent in pathComponents) {
|
|
// 对 key 进行排序,这样可以把 ~ 放到最后
|
NSArray *subRoutesKeys =[subRoutes.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
|
return [obj1 compare:obj2];
|
}];
|
|
for (NSString* key in subRoutesKeys) {
|
if ([key isEqualToString:pathComponent] || [key isEqualToString:MGJ_ROUTER_WILDCARD_CHARACTER]) {
|
found = YES;
|
subRoutes = subRoutes[key];
|
break;
|
} else if ([key hasPrefix:@":"]) {
|
found = YES;
|
subRoutes = subRoutes[key];
|
NSString *newKey = [key substringFromIndex:1];
|
NSString *newPathComponent = pathComponent;
|
// 再做一下特殊处理,比如 :id.html -> :id
|
if ([self.class checkIfContainsSpecialCharacter:key]) {
|
NSCharacterSet *specialCharacterSet = [NSCharacterSet characterSetWithCharactersInString:specialCharacters];
|
NSRange range = [key rangeOfCharacterFromSet:specialCharacterSet];
|
if (range.location != NSNotFound) {
|
// 把 pathComponent 后面的部分也去掉
|
newKey = [newKey substringToIndex:range.location - 1];
|
NSString *suffixToStrip = [key substringFromIndex:range.location];
|
newPathComponent = [newPathComponent stringByReplacingOccurrencesOfString:suffixToStrip withString:@""];
|
}
|
}
|
parameters[newKey] = newPathComponent;
|
break;
|
}
|
}
|
|
// 如果没有找到该 pathComponent 对应的 handler,则以上一层的 handler 作为 fallback
|
if (!found && !subRoutes[@"_"]) {
|
return nil;
|
}
|
}
|
|
// Extract Params From Query.
|
NSArray* pathInfo = [url componentsSeparatedByString:@"?"];
|
if (pathInfo.count > 1) {
|
NSString* parametersString = [pathInfo objectAtIndex:1];
|
NSArray* paramStringArr = [parametersString componentsSeparatedByString:@"&"];
|
for (NSString* paramString in paramStringArr) {
|
NSArray* paramArr = [paramString componentsSeparatedByString:@"="];
|
if (paramArr.count > 1) {
|
NSString* key = [paramArr objectAtIndex:0];
|
NSString* value = [paramArr objectAtIndex:1];
|
parameters[key] = value;
|
}
|
}
|
}
|
|
if (subRoutes[@"_"]) {
|
parameters[@"block"] = [subRoutes[@"_"] copy];
|
}
|
|
return parameters;
|
}
|
|
- (void)removeURLPattern:(NSString *)URLPattern
|
{
|
NSMutableArray *pathComponents = [NSMutableArray arrayWithArray:[self pathComponentsFromURL:URLPattern]];
|
|
// 只删除该 pattern 的最后一级
|
if (pathComponents.count >= 1) {
|
// 假如 URLPattern 为 a/b/c, components 就是 @"a.b.c" 正好可以作为 KVC 的 key
|
NSString *components = [pathComponents componentsJoinedByString:@"."];
|
NSMutableDictionary *route = [self.routes valueForKeyPath:components];
|
|
if (route.count >= 1) {
|
NSString *lastComponent = [pathComponents lastObject];
|
[pathComponents removeLastObject];
|
|
// 有可能是根 key,这样就是 self.routes 了
|
route = self.routes;
|
if (pathComponents.count) {
|
NSString *componentsWithoutLast = [pathComponents componentsJoinedByString:@"."];
|
route = [self.routes valueForKeyPath:componentsWithoutLast];
|
}
|
[route removeObjectForKey:lastComponent];
|
}
|
}
|
}
|
|
- (NSArray*)pathComponentsFromURL:(NSString*)URL
|
{
|
NSMutableArray *pathComponents = [NSMutableArray array];
|
if ([URL rangeOfString:@"://"].location != NSNotFound) {
|
NSArray *pathSegments = [URL componentsSeparatedByString:@"://"];
|
// 如果 URL 包含协议,那么把协议作为第一个元素放进去
|
[pathComponents addObject:pathSegments[0]];
|
|
// 如果只有协议,那么放一个占位符
|
if ((pathSegments.count == 2 && ((NSString *)pathSegments[1]).length) || pathSegments.count < 2) {
|
[pathComponents addObject:MGJ_ROUTER_WILDCARD_CHARACTER];
|
}
|
|
URL = [URL substringFromIndex:[URL rangeOfString:@"://"].location + 3];
|
}
|
|
for (NSString *pathComponent in [[NSURL URLWithString:URL] pathComponents]) {
|
if ([pathComponent isEqualToString:@"/"]) continue;
|
if ([[pathComponent substringToIndex:1] isEqualToString:@"?"]) break;
|
[pathComponents addObject:pathComponent];
|
}
|
return [pathComponents copy];
|
}
|
|
- (NSMutableDictionary *)routes
|
{
|
if (!_routes) {
|
_routes = [[NSMutableDictionary alloc] init];
|
}
|
return _routes;
|
}
|
|
#pragma mark - Utils
|
|
+ (BOOL)checkIfContainsSpecialCharacter:(NSString *)checkedString {
|
NSCharacterSet *specialCharactersSet = [NSCharacterSet characterSetWithCharactersInString:specialCharacters];
|
return [checkedString rangeOfCharacterFromSet:specialCharactersSet].location != NSNotFound;
|
}
|
|
@end
|
|
|
|
@implementation DHRouter
|
|
|
+ (void)registerURLPattern:(NSString *)URLPattern toHandler:(DHRouterHandler)handler
|
{
|
[MGJRouter registerURLPattern:URLPattern toHandler:handler];
|
}
|
|
+ (void)registerURLPattern:(NSString *)URLPattern toObjectHandler:(DHRouterObjectHandler)handler
|
{
|
[MGJRouter registerURLPattern:URLPattern toObjectHandler:handler];
|
}
|
|
+ (void)deregisterURLPattern:(NSString *)URLPattern
|
{
|
[MGJRouter deregisterURLPattern:URLPattern];
|
}
|
|
|
+ (void)openURL:(NSString *)URL
|
{
|
[MGJRouter openURL:URL];
|
}
|
|
+ (void)openURL:(NSString *)URL completion:(void (^)(id result))completion
|
{
|
[MGJRouter openURL:URL completion:completion];
|
}
|
|
+ (void)openURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo completion:(void (^)(id result))completion
|
{
|
[MGJRouter openURL:URL withUserInfo:userInfo completion:completion];
|
}
|
|
+ (id)objectForURL:(NSString *)URL
|
{
|
return [MGJRouter objectForURL:URL];
|
}
|
|
|
+ (id)objectForURL:(NSString *)URL withUserInfo:(NSDictionary *)userInfo
|
{
|
return [MGJRouter objectForURL:URL withUserInfo:userInfo];
|
}
|
|
+ (BOOL)canOpenURL:(NSString *)URL
|
{
|
return [MGJRouter canOpenURL:URL];
|
}
|
|
+ (NSString *)generateURLWithPattern:(NSString *)pattern parameters:(NSArray *)parameters
|
{
|
return [MGJRouter generateURLWithPattern:pattern parameters:parameters];
|
}
|
@end
|