Building iOS Routers Step by Step

Building iOS Routers Step by Step

Continue from the previous article Mobile terminal routing layer design

Why talk about iOS routing?

The routing layer is actually designed in the same logical way, but the implementation of interface jump is closely related to the navigation mechanism on the Android and iOS platforms. The Android operating system has a natural architectural advantage. The Intent mechanism can assist in the interaction and communication between applications. It is a description of calling components and data transmission. This mechanism itself removes the dependency between code logic and interface, and only has data dependency. However, the interface navigation and transition mechanism of iOS mostly depends on the implementation of UI components. Therefore, the implementation of routing on the iOS side is more representative in solving this problem.

In fact, to put it bluntly, the core problem that the routing layer solves is that the original calls between interfaces or components must be interdependent, requiring the import of the target header file and the need to understand the logic of the target object. Now, all of them are transferred through routing, relying only on routing, or relying on some message passing mechanisms that do not even rely on routing. Secondly, the core logic of routing is target matching. For external calls, how the URL matches the Handler is the most important, and regular expressions are inevitably used for matching. After understanding these key points, you will have a purpose for design, let's do it~

Design class diagram:

RouteClassMap.png

There are several categories here:

  1. WLRRouteRequest, a request at the routing layer, whether it is an external call across applications or an internal call, finally forms a routing request, which contains the query parameters and path parameters on the URL, the native parameters directly passed in during the internal call, and the callback block reserved by the request initiator for the target
  2. WLRRouteHandler, the handler of the routing layer, receives a WLRRouteRequest object to complete whether it is an interface jump, component loading, or internal logic
  3. WLRRouter, the core object of routing, holds registered Handlers, such as Handlers responsible for interface jumps, Handlers responsible for component loading, Handlers responsible for APIs, etc. The role of routing is to match the URL passed in by external calls or the target passed in by internal calls with the corresponding handlers internally, and then call the life cycle method to complete the processing process. Of course, there is also route middleware in the figure, which is actually a reserved AOP port for later expansion.
  4. WLRRouteMatcher is used to process whether the URL of the external call can match the preset regular expression. In WLRRouter, each time a handler is registered, a WLRRouteMatcher will be generated with a URL matching expression.
  5. WLRRegularExpression, inherited from NSRegularExpression, is used to match URLs. There is a WLRRegularExpression object inside WLRRouteMatcher. WLRRouteMatcher accepts a URL and uses WLRRegularExpression to generate a WLRMatchResult object to determine whether the match is successful. If a match is successful, the path parameters on the URL are taken out.
  6. WLRMatchResult, used to describe the matching result of WLRRegularExpression, including path parameters

Workflow:

  1. App starts instantiating the WLRRouter object
  2. Instantiate a WLRRouteHandler object
  3. The WLRRouter object mounts a WLRRouteHandler instance corresponding to the URL expression. The WLRRouter generates a WLRRouteMatcher object internally, corresponding to the URL expression.
  4. The URL and callback of the external call are passed into the WLRRouter object
  5. The WLRRouter object traverses the matching expression of the URL held internally and finds each WLRRouteMatcher object, passing the URL in to see if it can return a WLRRouteRequest object
  6. Pass the WLRRouteRequest object to the corresponding WLRRouteHandler object
  7. The WLRRouteHandler object finds the TargetViewController and SourceViewController according to the WLRRouteRequest, and completes parameter transfer and view transition in the life cycle function.

WLRRouteRequest:

Knowing the above, we start with WLRRouteRequest.

In fact, WLRRouteRequest is similar to NSURLRequest, but WLRRouteRequest inherits NSObject and implements the NSCopying protocol, which is roughly as follows:

  1. #import
  2.   
  3. @interface WLRRouteRequest: NSObject
  4. // URL for external calls
  5. @property (nonatomic, copy, readonly) NSURL *URL;
  6. //URL expression, for example, the expression for calling the login interface can be: AppScheme:// user /login/138********, then the URL matching expression can be: /login/:phone([0-9]+), the path must start with /login, followed by the phone number digits 0-9, of course, you can also directly write the regular matching of the phone number
  7. @property(nonatomic,copy)NSString * routeExpression;
  8. //If the URL is AppScheme:// user /login/138********?/callBack= "" , then this callBack appears here
  9. @property (nonatomic, copy, readonly) NSDictionary *queryParameters;
  10. //Here will appear {@ "phone" :@ "138********" }
  11. @property (nonatomic, copy, readonly) NSDictionary *routeParameters;
  12. //This contains the native parameters passed by the internal call
  13. @property (nonatomic, copy, readonly) NSDictionary *primitiveParams;
  14. //Automatically detect the callBack URL of the stolen callback
  15. @property (nonatomic, strong) NSURL *callbackURL;
  16. //The target viewcontroller or component can use this
  17. @property(nonatomic,copy)void(^targetCallBack)(NSError *error,id responseObject);
  18. //To indicate whether the request has been consumed
  19. @property(nonatomic)BOOL isConsumed;
  20. //Simple method, use the following index method to get parameters
  21. - (id)objectForKeyedSubscript:(NSString *) key ;
  22. // Initialization method
  23. -(instancetype)initWithURL:(NSURL *)URL routeExpression:(NSString *)routeExpression routeParameters:(NSDictionary *)routeParameters primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError * error,id responseObject))targetCallBack;
  24. -(instancetype)initWithURL:(NSURL *)URL;
  25. //Default completion target callback
  26. -(void)defaultFinishTargetCallBack;
  27. @ end  

NSURLRequest should actually be an object of value type, so there is nothing much to say about the implementation of the object to implement the copy protocol. You can just check it in the source code.

WLRRouteHandler

  1. #import
  2. @class WLRRouteRequest;
  3. @interface WLRRouteHandler : NSObject
  4. // About to handle a request
  5. - (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request;
  6. //According to the request, retrieve the target view controller to be called
  7. -(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request;
  8. //According to the request, retrieve the source view controller
  9. -(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request;
  10. //Start the transition
  11. -(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;
  12. @ end  

When the WLRRouter object completes the URL matching and generates the Request, and finds the Handler, it first calls - (BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request; to determine whether the handler is willing to handle it. If so, it calls -(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error;, and internally obtains the targetViewController and SourceViewController through convenient methods, and then performs the transition. The core method is implemented as follows:

  1. -(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error{
  2. UIViewController * sourceViewController = [self sourceViewControllerForTransitionWithRequest:request];
  3. UIViewController * targetViewController = [self targetViewControllerWithRequest:request];
  4. if ((![sourceViewController isKindOfClass:[UIViewController class]])||(![targetViewController isKindOfClass:[UIViewController class]])) {
  5. *error = [NSError WLRTransitionError];
  6. return   NO ;
  7. }
  8. if (targetViewController != nil) {
  9. targetViewController.wlr_request = request;
  10. }
  11. if ([self preferModalPresentationWithRequest:request]||![sourceViewController isKindOfClass:[UINavigationController class]]) {
  12. [sourceViewController presentViewController:targetViewController animated:YES completion:nil];
  13. }
  14. else if ([sourceViewController isKindOfClass:[UINavigationController class]]){
  15. UINavigationController * nav = (UINavigationController *)sourceViewController;
  16. [nav pushViewController:targetViewController animated:YES];
  17. }
  18. return YES;
  19. }
  20. - (BOOL)preferModalPresentationWithRequest:(WLRRouteRequest *)request;{
  21. return   NO ;
  22. }

Here we make a judgment based on the type of SourceController. In fact, the information of the request object is sufficient to determine how the target view should be opened. Essentially, the URL matching expression is strongly related to the business and the UI interaction logic. In the implementation of the transitionWithRequest method, you can inherit it and rewrite the transition process. You can even set up your own iOS7 custom transition, provide an animation controller and an object that implements the transition protocol, and then you can control the internal implementation of the Appp as a whole.

WLRRegularExpression

This class inherits NSRegularExpression

  1. #import
  2. @class WLRMatchResult;
  3. @interface WLRRegularExpression : NSRegularExpression
  4. // Pass in a URL and return a matching result
  5. -(WLRMatchResult *)matchResultForString:(NSString *)string;
  6. //Create a WLRRegularExpression instance based on a URL expression
  7. +(WLRRegularExpression *)expressionWithPattern:(NSString *)pattern;
  8. @ end  

The main function of this object is to pass in a URL to see if it matches, and remove the path parameters declared on the expression from the URL.

For example, if the URL matching expression we set is: login/:phone([0-9]+), then the URL such as AppScheme://user/login/138** should be matched, and the mobile phone number 138 should be taken out and matched to phone. This process must use the function of grouping and extracting substrings of regular expressions. :phone is the name of the key corresponding to the agreed value of the extracted substring. In fact, the regular expression of this URL should be: /login/([0-9]+)$, so the WLRRegularExpression object needs to know the keys of all substrings that need to be extracted and convert the URL matching expression into a real regular expression.

  1. -(instancetype)initWithPattern:(NSString *)pattern options:(NSRegularExpressionOptions)options error:(NSError * _Nullable __autoreleasing *)error{
  2. //In the initialization method, convert the URL matching expression pattern into a real regular expression
  3. NSString *transformedPattern = [WLRRegularExpression transfromFromPattern:pattern];
  4. //Initialize the parent class with the converted result
  5. if (self = [super initWithPattern:transformedPattern options:options error:error]) {
  6. //At the same time, save the key of the substring value to be extracted into the array
  7. self.routerParamNamesArr = [[self class] routeParamNamesFromPattern:pattern];
  8. }
  9. return self;
  10. }
  11. //Convert to regular expression
  12. +(NSString*)transfromFromPattern:(NSString *)pattern{
  13. //Copy the pattern
  14. NSString * transfromedPattern = [NSString stringWithString:pattern];
  15. //Use the regular expression :[a-zA-Z0-9-_][^/]+ to extract the substring key of the expression matched by the URL, that is, for a pattern like /login/:phone([0-9]+)/: name [a-zA-Z-_], you need to extract :phone([0-9]+) and : name [a-zA-Z-_]
  16. NSArray * paramPatternStrings = [self paramPatternStringsFromPattern:pattern];
  17. NSError *err;
  18. // Then according to the regular expression: [a-zA-Z0-9-_] +, remove all keys with extracted substrings , for example, remove: phone ([0-9] +) and change: phone to ([0-9] +)
  19. NSRegularExpression * paramNamePatternEx = [NSRegularExpression regularExpressionWithPattern:WLRRouteParamNamePattern options:NSRegularExpressionCaseInsensitive error:&err];
  20. for (NSString * paramPatternString in paramPatternStrings) {
  21. NSString * replaceParamPatternString = [paramPatternString copy];
  22. NSTextCheckingResult * foundParamNamePatternResult =[paramNamePatternEx matchesInString:paramPatternString options:NSMatchingReportProgress range:NSMakeRange(0, paramPatternString.length)].firstObject;
  23. if (foundParamNamePatternResult) {
  24. NSString *paramNamePatternString =[paramPatternString substringWithRange: foundParamNamePatternResult.range];
  25. replaceParamPatternString = [replaceParamPatternString stringByReplacingOccurrencesOfString:paramNamePatternString withString:@ "" ];
  26. }
  27. if (replaceParamPatternString.length == 0) {
  28. replaceParamPatternString = WLPRouteParamMatchPattern;
  29. }
  30. transfromedPattern = [transfromedPattern stringByReplacingOccurrencesOfString:paramPatternString withString:replaceParamPatternString];
  31. }
  32. if (transfromedPattern.length && !([transfromedPattern characterAtIndex:0] == '/' )) {
  33. transfromedPattern = [@ "^" stringByAppendingString:transfromedPattern];
  34. }
  35. //The $ sign should be used at the end
  36. transfromedPattern = [transfromedPattern stringByAppendingString:@ "$" ];
  37. //Finally, /login/:phone([0-9]+) will be converted to login/([0-9]+)$
  38. return transfromedPattern;
  39. }

When the Matcher object matches a URL

  1. -(WLRMatchResult *)matchResultForString:(NSString *)string{
  2. //First, match the URL using its own method to get an array of NSTextCheckingResult results
  3. NSArray * array = [self matchesInString:string options:0 range:NSMakeRange(0, string.length)];
  4. WLRMatchResult * result = [[WLRMatchResult alloc]init];
  5. if (array. count == 0) {
  6. return result;
  7. }
  8. result.match = YES;
  9. NSMutableDictionary * paramDict = [NSMutableDictionary dictionary];
  10. //Traverse the NSTextCheckingResult results
  11. for (NSTextCheckingResult * paramResult in array) {
  12. //It is convenient to extract the array of keys of the substrings during initialization
  13. for ( int i = 1; i<paramResult.numberOfRanges&&i <= self.routerParamNamesArr. count ;i++ ) {
  14. NSString * paramName = self.routerParamNamesArr[i-1];
  15. //Take out the value, and then put the key and value into paramDict
  16. NSString * paramValue = [string substringWithRange:[paramResult rangeAtIndex:i]];
  17. [paramDict setObject:paramValue forKey:paramName];
  18. }
  19. }
  20. //Finally assign the value to the WLRMatchResult object
  21. result.paramProperties = paramDict;
  22. return result;
  23. }

The core code has more than 80 lines in total, and you can read the source code in detail.

WLRRouteMatcher

  1. #import
  2. @class WLRRouteRequest;
  3. @interface WLRRouteMatcher : NSObject
  4. // Pass in the URL matching expression and get a matcher instance
  5. +(instancetype)matcherWithRouteExpression:(NSString *)expression;
  6. // Pass in the URL. If it matches, a WLRRouteRequest object is generated and various parameters are parsed and carried by WLRRouteRequest.
  7. -(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack;
  8. @ end  

The properties are as follows:

  1. //scheme
  2. @property(nonatomic,copy) NSString * scheme;
  3. //WLRRegularExpression instance
  4. @property(nonatomic,strong)WLRRegularExpression * regexMatcher;
  5. //Matching expression
  6. @property(nonatomic,copy)NSString * routeExpressionPattern;

Initialization method:

  1. -(instancetype)initWithRouteExpression:(NSString *)routeExpression{
  2. if (![routeExpression length]) {
  3. return nil;
  4. }
  5. if (self = [super init]) {
  6. // Take out the scheme and path parts separately
  7. NSArray * parts = [routeExpression componentsSeparatedByString:@ "://" ];
  8. _scheme = parts. count >1?[parts firstObject]:nil;
  9. _routeExpressionPattern =[parts lastObject];
  10. //Use the path part as a URL matching expression to generate a WLRRegularExpression instance
  11. _regexMatcher = [WLRRegularExpression expressionWithPattern:_routeExpressionPattern];
  12. }
  13. return self;
  14. }

Matching method:

  1. -(WLRRouteRequest *)createRequestWithURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void (^)(NSError *, id))targetCallBack{
  2. NSString * urlString = [NSString stringWithFormat:@ "%@%@" ,URL.host,URL.path];
  3. if (self.scheme.length && ![self.scheme isEqualToString:URL.scheme]) {
  4. return nil;
  5. }
  6. //Call self.regexMatcher to pass in the URL and get the WLRMatchResult result to see if it matches
  7. WLRMatchResult * result = [self.regexMatcher matchResultForString:urlString];
  8. if (!result.isMatch) {
  9. return nil;
  10. }
  11. //If it matches, pass the result.paramProperties path parameter to initialize a WLRRouteRequest instance
  12. WLRRouteRequest * request = [[WLRRouteRequest alloc]initWithURL:URL routeExpression:self.routeExpressionPattern routeParameters:result.paramProperties primitiveParameters:primitiveParameters targetCallBack:targetCallBack];
  13. return request;
  14. }

WLRRouter

  1. @class WLRRouteRequest;
  2. @class WLRRouteHandler;
  3. @interface WLRRouter : NSObject
  4. //Register the URL matching expression of the block callback, which can be used for internal calls
  5. -(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest * request))routeHandlerBlock forRoute:(NSString *)route;
  6. //Register a URL matching expression route corresponding to WLRRouteHandler
  7. -(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route;
  8. //Judge whether the URL can be handled
  9. -(BOOL)canHandleWithURL:(NSURL *)url;
  10. -(void)setObject:(id)obj forKeyedSubscript:(NSString *) key ;
  11. -(id)objectForKeyedSubscript:(NSString *) key ;
  12. //Call the handleURL method, passing in the URL, native parameters, targetCallBack, and the matching completionBlock
  13. -(BOOL)handleURL:(NSURL *)URL primitiveParameters:(NSDictionary *)primitiveParameters targetCallBack:(void(^)(NSError *, id responseObject))targetCallBack withCompletionBlock:(void(^)(BOOL handled, NSError *error))completionBlock;

In the implementation section, there are three properties:

  1. //Each URL's matching expression route corresponds to a matcher instance and is placed in the dictionary
  2. @property(nonatomic,strong)NSMutableDictionary * routeMatchers;
  3. //Each URL matching expression route corresponds to a WLRRouteHandler instance
  4. @property(nonatomic,strong)NSMutableDictionary * routeHandles;
  5. //Each URL matching expression route corresponds to a callback block
  6. @property(nonatomic,strong)NSMutableDictionary * routeblocks;

When the Route is hung in the Handler and callback block:

  1. -(void)registerBlock:(WLRRouteRequest *(^)(WLRRouteRequest *))routeHandlerBlock forRoute:(NSString *)route{
  2. if (routeHandlerBlock && [route length]) {
  3. //First add a WLRRouteMatcher instance
  4. [self.routeMatchers setObject:[WLRRouteMatcher matcherWithRouteExpression:route] forKey:route];
  5. //Delete the handler object corresponding to the route
  6. [self.routeHandles removeObjectForKey:route];
  7. //Store routeHandlerBlock and route in the corresponding dictionary
  8. self.routeblocks[route] = routeHandlerBlock;
  9. }
  10. }
  11. -(void)registerHandler:(WLRRouteHandler *)handler forRoute:(NSString *)route{
  12. if (handler && [route length]) {
  13. //First generate the WLRRouteMatcher instance corresponding to the route
  14. [self.routeMatchers setObject:[WLRRouteMatcher matcherWithRouteExpression:route] forKey:route];
  15. //Delete the block callback corresponding to the route
  16. [self.routeblocks removeObjectForKey:route];
  17. //Set the handler corresponding to the route
  18. self.routeHandles[route] = handler;
  19. }
  20. }

Next, improve the handle method:

As we can see above, Route encapsulates the matching logic into the WLRRouteMatcher object separately, generates a WLRRouteRequest instance with the matching result to carry sufficiently complete data, and encapsulates the actual processing of the view controller transition or component loading or the handle business that may be expanded in the future into the WLRRouteHandler instance. The matching logic and the corresponding processing logic are cleanly separated, and the matching logic can shape the business matching separately. The processing logic can better handle the callback business by inheriting, extending or flushing the life cycle function of WLRRouteHandler. If WLRRouteHandler cannot provide enough scalability, block callbacks can be used to expand it to the maximum extent.

The above is the overall implementation of the routing part.

Transition Extensions

In WLRRouteHandler, we can actually control the transition of the page jumps passed by the route separately.

  1. -(UIViewController *)targetViewControllerWithRequest:(WLRRouteRequest *)request{
  2. }
  3. -(UIViewController *)sourceViewControllerForTransitionWithRequest:(WLRRouteRequest *)request{
  4. }
  5. -(BOOL)transitionWithRequest:(WLRRouteRequest *)request error:(NSError *__autoreleasing *)error{
  6.   
  7. }

Doesn't this lifecycle function look like the protocol setting of UIViewControllerContextTransitioning transition context? - (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key; method enables the context to provide the target controller and source controller. In fact, you can completely customize a subclass in the handler. In the transitionWithRequest method, set the proxy that complies with UIViewControllerTransitioningDelegate, and then provide the animation controller that complies with UIViewControllerAnimatedTransitioning. Then customize the transition context and implement the custom UI transition. The corresponding matching logic is irrelevant. We can control the global page transition effect in the router. Students who are not familiar with custom transitions, please refer to my previous article:

ViewController Transitions of ContainerViewController

Routing security

There are two ways to do it

  1. In the WLRRouteHandler instance, -(BOOL)shouldHandleWithRequest:(WLRRouteRequest *)request can check the parameters in the request, such as verifying the source or verifying the integrity of the service parameters.
  2. The handleURL method in the WLRRouter instance will add middleware support in the subsequent WLRRoute 0.0.2 version. That is, before finding the handler, the middleware will be called back in the order of middleware registration. We can implement risk control services, authentication mechanisms, encryption and signature verification, etc. in the middleware.

Routing efficiency

The route we have implemented is a synchronous blocking one. There may be some problems when dealing with concurrency, or after registering more route expressions, the traversal and matching process will lose performance. A better implementation method is to modify the route to an asynchronous non-blocking one, but all APIs must be replaced with asynchronous APIs. Let's start with the synchronous one, and then slowly provide the asynchronous version of the route.

Use of Routing

When most apps practice MVVM architecture or the more complex VIPER architecture, in addition to the urgent need for a more decoupled message passing mechanism, how to better separate the acquisition of target entities and cooperate with the transition logic of the UIKit layer is a more complex challenge. Routing actually acts as the more decoupled target acquisition logic in the ViewModel of MVVM and the Router layer in VIPER. All calls to P and V are forwarded by the Router.

In the implementation of engineering transformation for the purpose of componentization, how to extract individual businesses as components and better manage the dependencies between businesses requires the use of a less invasive Route. The intrusion of WLRRoute lies in the transitionWithRequest logic of WLRRouteHandler. Through an extension of UIViewController, the WLRRouteRequest object is set to the target business through targetViewController.wlr_request = request;. However, despite this, you can still rewrite the transitionWithRequest method of WLRRouteHandler to build your own parameter passing method. This depends entirely on how you can better use routing without the business being aware of it.

Finally, attach the code address:

If you like it, give it a star...

https://github.com/Neojoke/WLRRoute

<<:  Mobile terminal routing layer design

>>:  Use the okhttp framework to implement user login including verification code and maintain session operation (Part 1)

Recommend

Why do carp love to jump over the Dragon Gate? Are there other fish like this?

Review expert: Li Weiyang, a well-known popular s...

How to make https certificate and HTTPS?

1. Why must we upgrade to HTTPS? The HTTP protoco...

How to attract the core users you want!

The core users we are talking about probably refe...

5 major scenes and 11 camera techniques for short video shooting

Why do short videos shot by others get millions o...

How to make 100 yuan a day, and what side job can make 3,000 yuan a month?

People in the workplace always have the idea and ...

Lessons from Momo’s overseas market promotion!

On August 27, Momo released its second quarter 20...

Full-link analysis of the “Juhuasuan” e-commerce marketing channel!

Each e-commerce platform is divided into differen...