Routing is an effective tool for decoupling between modules. Routing is an essential part of componentized development. At present, most routing tools on iOS are based on URL matching, with obvious advantages and disadvantages. This article will present a more native and secure design, which features:

  • Use protocol to find modules when routing
  • Modules can be fixed with dependency injection and runtime dependency injection
  • Interface adaptation and forwarding between different modules is supported, so there is no need to associate with a fixed protocol
  • Increase type safety while fully decoupling
  • You can remove routes that have been executed
  • Encapsulates UIKit interface jump methods that can jump and remove with one click
  • Support for storyboards, support for any other module
  • You can detect most errors when the interface jumps

If you want a fully decoupled, type-safe router with dependency injection, this is the best solution you can find.

This routing tool is designed to live up to the VIPER pattern and to provide dependency injection for VIPER, but it can also be used for MVC, MVP, MVVM without any limitations.

Tool and Demo address: ZIKRouter.

directory

  • The function of the Router
    • The route is missing
    • Looking for module
    • Declare dependencies and interfaces
    • Builder and dependency injection
  • The existing Router
    • URL Router
      • advantages
        • Extremely dynamic
        • Unified multi-terminal routing rules
        • Adapter URL scheme
      • disadvantages
        • Not suitable for generic modules
        • Poor safety
        • Maintenance difficulties
    • Protocol Router
      • advantages
        • Good security and easy maintenance
        • Applies to all modules
        • Declare dependencies gracefully
      • disadvantages
        • Dynamic finite
        • Additional adaptation to the URL Scheme is required
      • Does Protocol cause coupling?
        • Business design interconnectedness
        • Required InterfaceProvided Interface
    • Target-Action
      • advantages
      • disadvantages
    • UIStoryboardSegue
    • conclusion
  • The characteristics of ZIKRouter
    • Discrete management
    • Freely define routing parameters
    • Example Remove a route that has been executed
    • Obtain the corresponding module through protocol
      • Protocol Indicates the matching identifier
      • Many-to-one matching
    • Dependency injection and dependency declarations
      • Fixed dependencies and runtime dependencies
      • Declare it directly in the header file
    • Use generics to specify a specific router
    • Type safety
      • Pass in the correct protocol
      • Covariant and contravariant generics
    • Use Adapter compatible interfaces
      • forProvidedAdd the moduleRequired Interface
      • Forwarding interfaces with intermediaries
    • Encapsulate UIKit jump and remove methods
      • Encapsulate the routing method of iOS
      • identifyadaptativeType routing
      • Supports user-defined routes
      • About the jump method in Extension
    • Support the storyboard
    • AOP
    • Route error check
    • Support for any module
    • performance
  • Project address and Demo

The function of the Router

First of all, we need to clarify why we need Router, what benefits Router can bring and what problems it can solve. What kind of Router do we need?

The route is missing

Without routing, interface jump code can easily lead to intermodule coupling.

When performing a UI jump in iOS, we use the jump method provided on UIViewController:

[sourceViewController.navigationController pushViewController:destinationViewController animated:YES];
Copy the code
[sourceViewController presentViewController:destinationViewController animated:YES completion:nil];
Copy the code

If it is direct import destinationViewController header files for reference, will cause and coupling destinationViewController module. Similarly, such coupling occurs when one module references another. So we need a way to get destinationViewController, but cannot produce direct reference to it.

This is where the “find module” functionality provided by routing is needed. Get the destination module in some dynamic way.

So how does routing address module coupling? In the previous VIPER tutorial, routing has these main responsibilities:

  • Find the specified module to perform the specific routing operation
  • Declare module dependencies
  • Declare the external interface of the module
  • Dependency injection is performed on each part of the module

Through these functions, complete decoupling between modules can be achieved.

Looking for module

The most important function of routing is to provide a way to find a specific module. This scheme is loosely coupled, and the acquired module can be replaced at any time by another module with the same function at the other end, thus achieving decoupling between the two modules.

There are only a limited number of ways to implement finding modules:

  • Use a string identifier to identify a corresponding interface (URL Router, UIStoryboardSegue)
  • Use objective-C runtime features to call CTMediator directly.
  • Use a protocol to match an interface (mogujie’s second route and Ali’s BeeHive) so that the destination module can be transmitted more securely

The pros and cons of each of these options will be discussed later.

Declare dependencies and interfaces

A module A sometimes needs to use the functions of other modules, such as the most common log function. Different apps have different log modules. If module A has A high requirement for universality, the log method should not be written in module A, but should be called externally. Now this module A is dependent on A log module. App needs to know its dependency when using module A, so it can inject its dependency before using module A.

When using package management tools such as Cocoapods to configure dependencies between different modules, modules are generally strongly coupled and correspond to each other. Replacing a module is troublesome and can easily affect the whole body. This is fine if it’s a single functional module that really needs to rely on other specific libraries. However, if one business module references another business module, coupling should be avoided as much as possible. Because different business modules are generally handled by different people, you should avoid situations where a simple change to a business module (such as changing the name of a method or property) causes the business module that references it to have to change as well.

At this point, the business module needs to declare the modules it needs to rely on in the code, so that the APP can provide these modules when using, so as to fully decouple.

Sample code:

@protocol ZIKLoginServiceInput <NSObject>
- (void)loginWithAccount:(NSString *)account
                password:(NSString *)password
                 success:(void(^_Nullable)(void))successHandler
                   error:(void(^_Nullable)(void))errorHandler;
@end
Copy the code
@ interface ZIKNoteListViewController () / / notes need to login to view the interface, so the statement in the header file, @Property (nonatomic, strong) id<ZIKLoginServiceInput> loginService; @endCopy the code

This declaration relies on work that is actually the responsibility of the module’s Builder. In most cases, an interface module has not only one UIViewController, but also several other managers or services. These roles have their own dependencies, which are declared by the Builder of the module, and then set the dependencies inside the Builder. However, in the VIPER tutorial in the previous article, we added the Responsibilities of the Builder to the Router and had each module provide its own Router. So here, the Router is a discrete design, not a singleton. The Router is in charge of all routes. The advantage of this is that each module can fully customize and control its own routing process.

You can declare dependencies, which means you can also declare a module’s external interface. The two are very similar, so I won’t repeat them.

Builder and dependency injection

At the same time of the implementation of routing, the module is built with Builder, and dependency injection is carried out for each role in the module during the construction. When you call a module, you don’t need a simple concrete class, you need a concrete class in a completed module. Before using this module, the module needs to do some initialization operations, such as setting the dependency relationship between various roles in VIPER, which is an initialization operation. Therefore, the use of routing to obtain a module in the class, must be through the module Builder. Many routing tools are missing this feature.

You can think of dependency injection simply as passing arguments to the destination module. When jumping to the interface or using a module, you often need to set some parameters of the destination module, such as setting the delegate callback. At this point, you must call some method of the destination module, or pass some object. Since each module requires different parameters, most routers currently use dictionaries to wrap parameters around them. But there are better, more secure solutions, which are explained below.

You can also separate The Router, Builder, and Dependency Injector, but if the Router is a discrete design, it makes sense to give each Router to work with, while reducing the amount of code and providing fine-grained AOP.

The existing Router

With the responsibilities of routing sorted out, let’s now compare the various Router solutions available. I will not expand on the details of the implementation of each scheme, you can refer to this detailed article: iOS componentization – Route Design analysis.

URL Router

Most routers use a string of urls to indicate the interface to be opened. The code looks something like this:

// Register a URL to match the route handler save [URLRouter registerURL:@]"settings" handler:^(NSDictionary *userInfo) {
	UIViewController *sourceViewController = userInfo[@"sourceViewController"]; Id param = userInfo[@"param"]; UIViewController *settingViewController = [[settingViewController alloc] init]; [sourceViewController.navigationController pushViewController: settingViewController animated:YES];
}];
Copy the code
// Call route [URLRouter openURL:@"myapp://noteList/settings? debug=true" userInfo:params completion:^(NSDictionary *info) {

}];
Copy the code

Passing a string of urls opens the Settings screen of the noteList interface, wraps the parameters to be passed in a dictionary, and sometimes simply wraps UIKit methods like push and present to the caller.

The advantages and disadvantages of this approach stand out.

advantages

Extremely dynamic

This is the most dynamic solution, and you can even change the routing rules at run time to point to different interfaces. Multi-level page jumps can also be easily supported.

If your app is an e-commerce app, you need to do activities frequently, and the jump rules in the app often change, then it is very suitable to use the URL scheme.

Unified multi-terminal routing rules

The URL solution is the easiest to implement cross-platform. When iOS, Andorid, Web, and PC all route based on THE URL, routing rules of multiple terminals can be managed in a unified manner, reducing the maintenance and modification costs of multiple terminals, and enabling unskilled operators to easily and quickly modify routes.

As with the previous tip, this is a strong business related advantage. If you have a multi-faceted business need, using urls is also appropriate.

Adapter URL scheme

The URL scheme in iOS can communicate across processes to open a specified page in the app from outside the app. When all pages in the app can be opened using URL, it is directly compatible with URL Scheme without additional work.

disadvantages

Not suitable for generic modules

The URL Router design is only suitable for UI modules, not for components of other functional modules. Calls to functional modules do not need to be so dynamic. Unless hot update is required, calls to a module should always be stable within a version, even if decoupling between modules is required, it should not be done this way.

Poor safety

String matching is not compile-time checking, and when a page configuration error occurs, it can only be detected at run time. If a developer accidentally adds a space to the string, it won’t be noticed at compile time. You can reduce this error by using macro definitions.

Maintenance difficulties

There is no efficient way to declare interfaces, only to look them up in documentation, which must be written carefully against strings and their parameter types.

The parameter is passed through a dictionary, the parameter types are not guaranteed, and it is impossible to know exactly what parameters are required by the interface being called. When the destination module gets an interface update, changing the type and number of arguments, everything is changed, and without the compiler’s help, you can’t tell if something is missing. This can lead to significant maintenance and refactoring costs.

To solve this problem, Mogujie chose to use another Router and protocol to obtain the destination module and then call it to increase security.

Protocol Router

The scheme is also easy to understand. Change the string match to a Protocol match, and you get an object that implements a protocol.

The open source solution only sees BeeHive implementing this:

id<ZIKLoginServiceInput> loginService = [[BeeHive shareInstance] createService:@protocol(ZIKLoginServiceInput)];
Copy the code

advantages

Good security and easy maintenance

It is safe to call the protocol method on this object. The compiler’s type checking makes refactoring and modification more efficient.

Applies to all modules

Protocol is more in line with OC and Swift’s native design and can be used by any module, not just the UI module.

Declare dependencies gracefully

Module A needs the login module, but how can it declare this dependency? If you use the Protocol Router, you only need to define one attribute in the header file:

@property (nonatomic, string) id<ZIKLoginServiceInput> *loginService;
Copy the code

If the dependency is required and not optional, add it to the initialization parameter:

@interface ModuleA ()
- (instancetype)initWithLoginService:(id<ZIKLoginServiceInput>)loginService;
@end
Copy the code

The problem is, if there are a lot of such dependencies, the initialization method becomes very long. Therefore, it is better for the Builder to do fixed dependency injection and provide it externally. BeeHive does not currently provide dependency injection.

disadvantages

Dynamic finite

You can maintain a table of protocols and modules, use dynamic protocols to try to dynamically change routing rules, or encapsulate a LAYER of URL routers on top of protocol Routers for dynamic requirements.

Additional adaptation to the URL Scheme is required

Using a Protocol Router requires additional processing of the URL Scheme. However, this is normal, the resolution URL Scheme should be in a separate module.

Does Protocol cause coupling?

Many articles on this scenario will point out that the Protocol Router, in contrast to the URL Router, causes the caller to refer to the Protocol of the destination module, resulting in “coupling”. I think that’s a misinterpretation of decoupling.

To avoid coupling, we first need to figure out how much decoupling we need. My definition is: module A calls module B, and module A does not need to make any changes to the interface or implementation of module B when simple changes are made, or module B is replaced with module C with the same function. In this case, module A and module B can be considered decoupled.

Business design interconnectedness

Sometimes it makes sense to express the relationship between two modules.

When interface A needs to display A login interface, it may need to pass A prompt parameter to the login interface to display A string of prompts on the login interface. In this case, interface A requires the login interface to display the custom prompt when invoking the login interface. In service design, there is A strong correlation between the two modules. At this point, the URL Router is no different from the Protocol Router, including the target-action routing described below, which has coupling, but the Protocol Router can be easily modified to remove this coupling.

The Router URL:

[URLRouter openURL:@"login" userInfo:@{@"message": @"Please log in to view notes details."}];
Copy the code

Protocol Router:

@protocol LoginViewInput <NSObject> @property (nonatomic, copy) NSString *message; UIViewController<LoginViewInput> *loginViewController = [ProtocolRouter destinationForProtocol:@protocol(LoginViewInput)]; loginViewController.message = @"Please log in to view notes details.";
Copy the code

Because of dictionary parameter passing, the URL Router simply hides this interface association in the dictionary key. It implicitly uses the LoginViewInput interface when it uses @”message” in the parameter dictionary.

This business-design interconnectedness between modules is inevitable and does not need to be hidden. Hiding it is a recipe for trouble. If the attribute name of the login interface is changed from NSString *message to NSString *notifyString, the URL Router must also change its code when registering parameters. If register is executed and handled by the login interface itself, rather than by the App Context, then the key is fixed to @”notifyString”, which requires all callers to change the pass key to notifyString as well. Macros are used to reduce the amount of work caused by such changes, which can be dangerous without the help of the compiler. When the Protocol Router is modified, it can make full use of the compiler to check, ensuring 100% security.

Therefore, the URL Router does not decouple, but simply hides the interface association. The trouble comes when you need to modify or refactor, and when you replace macros, you have to double-check that there are no keys that use strings directly. Is it manageable to simply change the name? What if you need to add parameters? At this point, there is no way to check where parameter passes are missing. That’s the downside of dictionary passing.

For a discussion of this section, see Peak’s article: iOS Componentization Solutions.

The Protocol Router also needs to be modified in this case, but it can help you refactor safely and efficiently. And with a bit of refinement, you can do without it altogether. The solution is to separate the Protocol into Required Interface and Provided Interface.

Required InterfaceProvided Interface

A module’s Interface is distinguished between a Required Interface and a Provided Interface. The Required Interface is the Interface Required by the caller, and the Provided Interface is the Interface actually Provided by the caller.

In UML component diagrams, both concepts are clearly represented. The semicircle below is the Required Interface and the circle outside the box is the Provided Interface:

So how do you implement Required Interface and Provided Interface? As discussed in the previous article, the App Context should Interface within an Adapter so that the caller can continue to use the Required Interface internally, The Adapter is responsible for adapting the Required Interface to the modified Provided Interface.

Sample code:

@protocol ModuleARequiredLoginViewInput <NSObject> @property (nonatomic, copy) NSString *message; @ the end / / calling code in the Module A UIViewController < ModuleARequiredLoginViewInput > * loginViewController = [ZIKViewRouterToView(LoginViewInput) makeDestination]; loginViewController.message = @"Please log in to view notes details.";
Copy the code
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
Copy the code
// Adapter in App Context, Interface adaptation using Objective-C category or Swift extension @Interface LoginViewController (ModuleAAdapte) <ModuleARequiredLoginViewInput> @property (nonatomic, copy) NSString *message; @end @implementation LoginViewController (ModuleAAdapte) - (void)setMessage:(NSString *)message {
	self.notifyString = message;
}
- (NSString *)message {
	return self.notifyString;
}
@end
Copy the code

New and old interfaces are compatible with category, extension, NSProxy and other technologies. All the work is completed by the use of modules and the assembler App Context. If the LoginViewController already has its own message attribute, then the new login module is incompatible and must be modified by one of the parties. Of course, interface adaptation is limited in what it can do. For example, if an interface changes from synchronous to asynchronous, the two modules will not be compatible.

Therefore, if a module needs to be decoupled, its interfaces should be carefully designed so as not to introduce too many other module dependencies into the parameters.

Complete decoupling can only be achieved if the concept of Required Interface and Provided Interface is designed. This is missing from all current routing schemes.

Target-Action

In CTMediator scheme, the call to the module is encapsulated in target-Action, which makes use of the Objective-C Runtime feature, omits the registration and binding of target-action, and directly calls the method of the Target module through the CTMediator.

@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail
{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action:kCTMediatorActionNativFetchDetailViewController
                                                    params:@{@"key": @"value"}
                                         shouldCacheTarget:NO
                                        ];
    if([viewController isKindOfClass:[UIViewController Class]]) {// After the viewController is delivered, it can be selected as push or presentreturn viewController;
    } else{// Exception scenarios are handled here, depending on the productreturn [[UIViewController alloc] init];
    }
}
@end
Copy the code

– performTarget: action: params: shouldCacheTarget: method through NSClassFromString, get objective Target provided by the module class, then call the Target provided by the action, has realized the method call:

@implementation CTMediator - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary  *)params shouldCacheTarget:(BOOL)shouldCacheTarget { NSString *targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    Class targetClass;
    
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }
    
    SEL action = NSSelectorFromString(actionString);
    
    if(target == nil) {// This is one of the places to handle unresponsive requestsreturn. In practice, a fixed target could be assigned in advance to handle the request at this timereturn nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } elseActionString = [NSString stringWithFormat:@"Action_%@WithParams:", actionName];
        action = NSSelectorFromString(actionString);
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else{// This is where the unresponsive request is handled. If there is no response, try calling the notFound method corresponding to target to handle SEL action = NSSelectorFromString(@)"notFound:");
            if ([target respondsToSelector:action]) {
                return [self safePerformAction:action target:target params:params];
            } else{// This is also where unresponsive requests are handled. In the absence of notFound, the demo is directreturn. In practice, you can use the fixed target above mentioned. [self.cachedTarget removeObjectForKey:targetClassString];return nil;
            }
        }
    }
}
@end
Copy the code

advantages

  • Simple implementation, the entire implementation of the code is very small
  • Bypassing the route registration step can reduce memory consumption and time consumption, but also slightly reduce the performance of the call
  • Usage scenarios are not limited to interface modules; all modules can be invoked through intermediaries

disadvantages

  • Using dictionaries to pass parameters when calling actions cannot guarantee type safety and is difficult to maintain
  • Using the Runtime directly to call each other, it is difficult to distinguish clearlyRequired InterfaceandProvided Interface, so you can’t really decouple completely. As with the URL Router, the calling module must be modified when the destination module changes
  • Too much reliance on runtime features is incompatible with Swift’s type-safe design and cannot be implemented across multiple platforms

UIStoryboardSegue

Apple’s Storyboard actually has a routing API, but it’s very limited. Here is a brief introduction:

Implementation SourceViewController - (void)showLoginViewController {// Call the segue identifier defined in storyboard [self] performSegueWithIdentifier:@"presentLoginViewController"sender:nil]; } / / perform segue callback - identifier (BOOL) shouldPerformSegueWithIdentifier: (nsstrings *) sender: (nullable id) sender {returnYES; Void prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {// use [segue DestinationViewController] get purpose interface, and then to preach and} @ the end purpose interfaceCopy the code

UIStoryboardSegue is used with storyboard, storyboard defines some interface jump parameters like push, present, and so on, and the UIViewController that does the route gets a callback before it does the route, Lets the caller configure the parameters of the destination interface.

When you’re segueing in a storyboard, you’re actually jumping to a view controller that’s already configured for the interface, that’s all the parameters associated with the view, that’s already done dependency injection. But custom dependencies, we still need to inject them in our code, so it gives us another prepareForSegue sender callback.

I don’t recommend using segues because it leads to strong coupling. But we can draw lessons from UIStoryboardSegue sourceViewController, destinationViewController, encapsulation jump logic to segue subclasses, such as to perform dependency injection page design.

conclusion

After reviewing several routing tools, I’ve come to the conclusion that routing comes first based on business needs. When dynamic requirements are high, or multiple platforms are required for unified routing, I choose the URL Router. Otherwise, I prefer the Protocol Router. That’s largely consistent with Peak.

There is no mature open source solution for the Protocol Router. So I built a wheel to augment some of the requirements mentioned above.

The characteristics of ZIKRouter

Discrete management

Each module corresponds to one or more Router subclasses that manage their routing processes, including object generation, module initialization, route state management, AOP, and so on. When routing, you need to use the corresponding router subclass, instead of a singleton router controlling all routes. If you want to avoid the coupling that comes with referring to a subclass, you can use Protocol to dynamically obtain the Router subclass, or replace the subclass with a superclass + generic in the caller.

The reason for the discrete design is to allow each module to have full control over routing.

A simple implementation of a Router subclass is as follows:

@interface ZIKLoginViewRouter : ZIKViewRouter @end @implementation ZIKLoginViewRouter @end @implementation ZIKLoginViewRouter Because have not apply in the Swift + (void) registerRoutableDestination {[self registerView: [ZIKLoginViewController class]]. [self registerViewProtocol:ZIKRoutableProtocol(ZIKLoginViewProtocol)]; } // When routing is performed, Returns the corresponding viewController or UIView - (id) destinationWithConfiguration: (ZIKViewRouteConfiguration *) configuration {UIStoryboard  *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    ZIKLoginViewController *destination = [sb instantiateViewControllerWithIdentifier:@"ZIKLoginViewController"];
    returndestination; } + (BOOL)destinationPrepared:(UIViewController<ZIKLoginViewProtocol> *)destination {if(destination.loginService ! = nil) {return YES;
    }
    returnNO; } // initialize work - (void)prepareDestination:(UIViewController<ZIKLoginViewProtocol> *)destination configuration:(__kindof ZIKViewRouteConfiguration *)configuration {if(destination.loginService == nil) {//ZIKLoginService can also be dynamically retrieved by ZIKServiceRouter [ZIKLoginService new]; }} / / routing AOP callback + (void) the router (nullable ZIKViewRouter *) router willPerformRouteOnDestination (id) destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
@end
Copy the code

You can even return different destinations in different cases without the caller knowing about it. For example, if an alertViewRouter is compatible with UIAlertView and UIAlertController, it can use UIAlertController inside the router for iOS8 and up, and UIAlertView for iOS8 and up.

Control of all routes resides within the Router class and does not require any additional modifications by the module.

Freely define routing parameters

The configuration information of the route is stored in the Configuration and is passed in when the caller executes the route. The basic jump method is as follows:

/ / jump to the Login interface [ZIKLoginViewRouter performFromSource: self/jump/the interface between the source interface you: ^ (ZIKViewRouteConfiguration * config) { // Jump type, Supports push, presentModally, presentAsPopover, performSegue, show, showDetail, addChild, addSubview, Custom, and getDestination config.routeType = ZIKViewRouteTypePush; config.animated = NO; Config. PrepareDestination = ^ (id < ZIKLoginViewProtocol > destination) {/ / jump before configuration interface}; RouteCompletion = ^(id<NoteEditorProtocol> destination) {// Jump succeeded and end processing}; Config. PerformerErrorHandler = ^ (ZIKRouteAction routeAction, NSError * error) {/ / jump to deal with failure, detailed information have failed};}];Copy the code

Configuration can only be configured in the initial block and cannot be modified after the block exits. You can also add more customization using a Configuration subclass.

If you don’t need complex configuration, you can just use the simplest jump:

[ZIKLoginViewRouter performFromSource:self routeType:ZIKViewRouteTypePush];
Copy the code

Example Remove a route that has been executed

You can initialize a router and assign it to another router:

/ / initialize the router self. LoginRouter = [[ZIKLoginViewRouter alloc] initWithConfiguring: ^ (ZIKViewRouteConfiguration * _Nonnull config) { config.source = self; config.routeType = ZIKViewRouteTypePush; }]; // Perform routingif ([self.loginRouter canPerform] == NO) {
    NSLog(@"Cannot execute route at this time :%@",self.loginRouter);
    return;
}
[self.loginRouter performRouteWithSuccessHandler:^{
    NSLog(@"performer: push success");
} performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) {
    NSLog(@"performer: push failed: %@",error);
}];
Copy the code

When you need to remove a displayed interface, or destroy a module, you can call the Remove route method to remove it with one click:

if ([self.loginRouter canRemove] == NO) {
    NSLog(@"Cannot remove route at this time :%@", self.loginRouter);
    return;
}
[self.loginRouter removeRouteWithSuccessHandler:^{
    NSLog(@"performer: pop success");
} performerErrorHandler:^(ZIKRouteAction routeAction, NSError * _Nonnull error) {
    NSLog(@"performer: pop failed,error:%@",error);
}];
Copy the code

Thus no need to distinguish between a pop, dismiss, removeFromParentViewController, removeFromSuperview method, etc.

Obtain the corresponding module through protocol

Protocol Indicates the matching identifier

We don’t want external references to the ZIKLoginViewRouter header to cause coupling, so the caller just needs to get a View Controller that conforms to the ZIKLoginViewProtocol, To hide the subclass, call the routing method provided by ZIKViewRouter on the subclass of ZIKLoginViewProtocol.

Using the ZIKViewRouterToView and ZIKViewRouterToModule macros, you can obtain the corresponding router subclass from protocol. The destination returned by the subclass must match ZIKLoginViewProtocol:

[ZIKViewRouterToView(ZIKLoginViewProtocol) performFromSource:self configuring:^(ZIKViewRouteConfiguration *config) { config.routeType = ZIKViewRouteTypePush; Config. PrepareDestination = ^ (id < ZIKLoginViewProtocol > destination) {/ / jump before configuration interface}; RouteCompletion = ^(id<ZIKLoginViewProtocol> destination) {};}];Copy the code

The ZIKLoginViewProtocol is the unique identifier of the LoginView module and cannot be used by other view controllers. You can register the same router with multiple protocols to distinguish between requiredProtocol and providedProtocol.

Many-to-one matching

Sometimes, if a third-party module or system module does not provide its own router, you can wrap a router around it. In this case, multiple different Routers can manage the same UIViewController or UIView class. These routers may provide different functions. For example, routerA may encapsulate UIAlertController, routerB may be compatible with UIAlertView and UIAlertController. How to distinguish and obtain two different routers?

For a router that provides a unique function, subclass configuration and conform the protocol for that function. The router can be obtained according to the protocol of the configuration:

[ZIKViewRouterToModule(ZIKCompatibleAlertConfigProtocol)
    performFromSource:self
    configuring:^(ZIKViewRouteConfiguration<ZIKCompatibleAlertConfigProtocol> * _Nonnull config) {
 	config.routeType = ZIKViewRouteTypeCustom;
 	config.title = @"Compatible Alert";
 	config.message = @"Test custom route for alert with UIAlertView and UIAlertController";
 	[config addCancelButtonTitle:@"Cancel" handler:^{
	 	NSLog(@"Tap cancel alert");
 	}];
 	[config addOtherButtonTitle:@"Hello" handler:^{
	 	NSLog(@"Tap hello button");
 	}];
 	config.routeCompletion = ^(id _Nonnull destination) {
	 	NSLog(@"show custom alert complete");
 	};
}];
Copy the code

If a module provides a router and the router is used for dependency injection and cannot be replaced by another router, you can declare the router as the only designated router for the module. When more than one router tries to manage the module, an assertion error occurs when it is started.

Dependency injection and dependency declarations

Fixed dependencies and runtime dependencies

Module dependency is divided into fixed dependency and runtime parameter dependency.

A fixed dependency is a fixed dependency in a module, which is similar to the dependency relationship between roles in VIPER. This dependence only need inside the router – prepareDestination: configuration: fixed configuration.

Runtime is dependent on the external of the incoming parameters, the configuration is responsible for the transfer, and also in – prepareDestination: configuration: the destination for the configuration and configuration. You can pair the Router with a Configuration subclass to add more customization.

If the dependency is simple, you can also have the router configure the destination directly, declare the router’s destination to comply with ZIKLoginViewProtocol, and have the caller set destination in prepareDestination. However, if the dependency involves passing model objects, and destination can’t touch those model objects due to the need to isolate the View and model, then configuration still needs to pass the dependency. The model is then passed inside the Router to the role responsible for managing the model.

Therefore, the Protocol of Configuration and Destination is responsible for dependency declarations and exposing interfaces. The caller only needs to pass in the parameters required by the Protocol or call some initialization methods. The caller does not care how the router uses and configures these dependencies.

Declare it directly in the header file

To declare a protocol to be the Config protocol or View Protocol of a router, simply make the protocol inherit from ZIKViewConfigRoutable or ZIKViewRoutable. This way, all dependency declarations can be made explicit in the header file instead of having to look it up in the document.

Use generics to specify a specific router

A module can dynamically fetch the Router internally using ZIKViewRouterToModule and ZIKViewRouterToView. It can also add a router attribute to the header file and let the Builder inject it.

How does a module declare to the Builder that it needs a router for a particular function? The answer is superclass + generics.

ZIKRouter supports specifying parameter types with generics. It can be used in OC like this:

// Note that this sample code is only used to demonstrate what generics mean, The actual runtime must use a ZIKViewRouter subclasses can [ZIKViewRouter < UIViewController *, ZIKViewRouteConfiguration < ZIKLoginConfigProtocol > * > performFromSource:self configuring:^(ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *config) { config.routeType = ZIKViewRouteTypePerformSegue;  config.configureSegue(^(ZIKViewRouteSegueConfiguration *segueConfig) { segueConfig.identifier = @"showLoginViewController";
    );
}];
Copy the code

ZIKViewRouter < UIViewController *, ZIKViewRouteConfiguration < ZIKLoginConfigProtocol > * > is a specifies the generic class, Angle brackets specify the destination and Configuration types of the router. This string of instructions amounts to a statement: It is a destination for UIViewController type, use ZIKViewRouteConfiguration < ZIKLoginConfigProtocol > * as perform routing configuration of the router class. You can add protocol to configuration to indicate that the configuration must comply with the specified protocol.

You can then declare a Router class with a parent class and a generic class whose Configuration conforms to a particular Config protocol. And Xcode will auto-complete it for you as you write it. This is a good way to hide subclasses, and it’s native syntax.

But since classes in OC are of Class type, you can only declare an instance attribute like this:

@property (nonatomic, strong) ZIKViewRouter<UIViewController *,ZIKViewRouteConfiguration<ZIKLoginConfigProtocol> *> *loginViewRouter;
Copy the code

The Builder can inject only one Router instance, not one Router class. So it’s not usually used in OC.

However, in a type-safe language like Swift, this pattern works much better. You can inject a router that conforms to a generic type:

/ / in the Builder injection alertRouter swiftSampleViewController. AlertRouter = Router.to(RoutableViewModule<ZIKCompatibleAlertConfigProtocol>())Copy the code
class SwiftSampleViewController: UIViewController {// After the routerClass is injected into the Builder, we can directly execute the route with the routerClass var alertRouter: ViewRouter<Any, ZIKCompatibleAlertConfigProtocol>! @IBAction functestInjectedRouter(_ sender: Any) {
        self.alertRouter.perform(
            from: self,
            configuring: { (config, prepareDestination, prepareModule) in
            prepareModule({ moduleConfig in/ / moduleConfig is ZIKCompatibleAlertConfigProtocol when type inference, again after judgment without coercion moduleConfig. Title ="Compatible Alert"
                moduleConfig.message = "Test custom route for alert with UIAlertView and UIAlertController"
                moduleConfig.addCancelButtonTitle("Cancel", handler: {
                print("Tap cancel alert")
                })
                moduleConfig.addOtherButtonTitle("Hello", handler: {
                    print("Tap Hello alert")})})}}Copy the code

Declares the ViewRouter < Any ZIKCompatibleAlertConfigProtocol > attributes, can be directly injected into a corresponding external router. You can use this design pattern to shift and centralize responsibility for getting the Router.

The Router can restrict its generics when defining them:

Objective-C:

@interface ZIKCompatibleAlertViewRouter : ZIKViewRouter<UIViewController *, ZIKViewRouteConfiguration<ZIKCompatibleAlertConfigProtocol> *>

@end
Copy the code

Swift:

class ZIKCompatibleAlertViewRouter: ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol> {

}
Copy the code

This allows the compiler to check that the router is correct on pass time.

Call security and type safety

The above demo has shown type-safe handling, which is designed by protocol and generics together. But there are some issues that need special attention.

Compile the inspection

When ZIKViewRouterToModule and ZIKViewRouterToView are used, the incoming protocol is checked for compilation. Ensure that the incoming protocol is routable and cannot be abused. It’s a bit more complicated, and there are two ways to do compile checking on Objective-C and Swift, which you can see in the source code.

Covariant and contravariant generics

Swift’s custom generics do not support covariation, so it is a bit strange to use.

letalertRouterClass: ZIKViewRouter<UIViewController, ZIKViewRouteConfiguration >. The Type / / compile error / / ZIKCompatibleAlertViewRouter Type is ZIKViewRouter < UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol>.Type alertRouterClass = ZIKCompatibleAlertViewRouter.selfCopy the code

Swift’s custom generics do not support subtype to parent type, So the ZIKViewRouter < UIViewController, ZIKViewRouteConfiguration & ZIKCompatibleAlertConfigProtocol >. The Type assigned to ZIKViewRouter < UIViewController, ZIKViewRouteConfiguration >. The Type, Type is a compilation error will occur. Oddly enough, the inverse inverse has no compilation errors. The Swift native collection type is covariant. Swift has been proposed to add covariant to custom generics since 2015, and has yet to support it. Custom generics are normally covariant in Objective-C.

So in Swift, another class is used to wrap the real Router, and this class can optionally specify generics.

Use Adapter compatible interfaces

You can obtain the same router using different protocols. Both requiredProtocol and providedProtocol can obtain the same router as long as they are declared.

Start by checking requiredProtocol and providedProtocol to determine that the functionality provided by the two interfaces is consistent. Otherwise it won’t work.

forProvidedAdd the moduleRequired Interface

RequiredProtocol is an external requirement that the destination module is additional compatible with the App Context interface in a subclass of ZIKViewAdapter.

@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable> @property (nonatomic, copy) NSString *message; @ the end / / calling code in the Module A UIViewController < ModuleARequiredLoginViewInput > * loginViewController = [ZIKViewRouterToView(LoginViewInput) makeDestination]; loginViewController.message = @"Please log in to view notes details.";
Copy the code
//Login Module Provided Interface
@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@end
Copy the code
//ZIKEditorAdapter. H, ZIKViewAdapter subclass @interface ZIKEditorAdapter: ZIKViewRouteAdapter @endCopy the code
@interface LoginViewController (ModuleAAdapte) @interface LoginViewController (ModuleAAdapte) <ModuleARequiredLoginViewInput> @property (nonatomic, copy) NSString *message; @end @implementation LoginViewController (ModuleAAdapte) - (void)setMessage:(NSString *)message {
	self.notifyString = message;
}
- (NSString *)message {
	returnself.notifyString; } @end @implementation ZIKEditorAdapter + (void)registerRoutableDestination { / / registered NoteListRequiredNoteEditorProtocol and ZIKEditorViewRouter match [ZIKEditorViewRouter registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)]; } @endCopy the code

Forwarding interfaces with intermediaries

If some delegate in protocol needs to be compatible:

@protocol ModuleARequiredLoginViewDelegate <NSObject>
- (void)didFinishLogin;
@end

@protocol ModuleARequiredLoginViewInput <ZIKViewRoutable>
@property (nonatomic, copy) NSString *message;
@property (nonatomic, weak) id<ModuleARequiredLoginViewDelegate> delegate;
@end
Copy the code
@protocol LoginViewDelegate <NSObject>
- (void)didLogin;
@end

@protocol ProvidedLoginViewInput <NSObject>
@property (nonatomic, copy) NSString *notifyString;
@property (nonatomic, weak) id<LoginViewDelegate> delegate;
@end
Copy the code

This kind of situation in the OC can hook – setDelegate: method, use NSProxy for message forwarding, for the corresponding ModuleARequiredLoginViewDelegate LoginViewDelegate message forwarding the message.

If the same method has different parameter types, we can replace the real router with a new router and insert a mediator into the new router, which is responsible for forwarding interfaces:

@implementation ZIKEditorMediatorViewRouter + (void)registerRoutableDestination { / / register NoteListRequiredNoteEditorProtocol, and new ZIKEditorMediatorViewRouter matching, Rather than a purpose in the module ZIKEditorViewRouter / / new ZIKEditorMediatorViewRouter is responsible for calling ZIKEditorViewRouter, Insert a mediator [self registerView:/* Mediator's class */]; [self registerViewProtocol:ZIKRoutableProtocol(NoteListRequiredNoteEditorProtocol)]; } - destinationWithConfiguration: (ZIKViewRouteConfiguration *) configuration (id) {/ / ZIKEditorViewRouter get real destination  id<ProvidedLoginViewInput> realDestination = [ZIKEditorViewRouter makeDestination]; / / get a mediator is responsible for forwarding ProvidedLoginViewInput and ModuleARequiredLoginViewInput id < ModuleARequiredLoginViewInput > mediator = MediatorForDestination(realDestination);return mediator;
}
@end
Copy the code

In general, it is not necessary to immediately separate all protocols into requiredProtocol and providedProtocol. The calling module and the destination module can share protocol temporarily, or simply change the name and separate protocol the first time a module needs to be replaced.

Encapsulate UIKit jump and remove methods

Encapsulate the routing method of iOS

ZIKViewRouter puts UIKit routing related methods:

  • -pushViewController:animated:
  • -presentViewController:animated:completion:
  • UIPopoverControllerthe-presentPopoverFromRect:inView:permittedArrowDirections:animated:
  • UIPopoverPresentationControllerThe configuration of the
  • -performSegueWithIdentifier:sender:
  • -showViewController:sender:
  • -showDetailViewController:sender:
  • -addChildViewController:
  • -addSubview:

All unified encapsulation, can be switched with enumeration:

[ZIKViewRouterToView(ZIKLoginViewProtocol)
    performFromSource:self routeType::ZIKViewRouteTypePush];
Copy the code

Corresponding enumeration values:

  • ZIKViewRouteTypePush
  • ZIKViewRouteTypePresentModally
  • ZIKViewRouteTypePresentAsPopover
  • ZIKViewRouteTypePerformSegue
  • ZIKViewRouteTypeShow
  • ZIKViewRouteTypeShowDetail
  • ZIKViewRouteTypeAddAsChildViewController
  • ZIKViewRouteTypeAddAsSubview
  • ZIKViewRouteTypeCustom
  • ZIKViewRouteTypeGetDestination

When removing a route, Also don’t have to judge the different call – popViewControllerAnimated: respectively, – dismissViewControllerAnimated: completion:, – dismissPopoverAnimated:, – removeFro MParentViewController, -RemoveFromSuperView, etc.

The ZIKViewRouter automatically calls the corresponding method internally.

identifyadaptativeType routing

– performSegueWithIdentifier: sender:, – showViewController: sender:, – showDetailViewController: sender: the adaptative routing method, The system ADAPTS UINavigationController and UISplitViewController to push, present, or other methods depending on the situation. When you call directly, you don’t know exactly which method you’re going to end up calling, so you can’t remove the interface.

ZIKViewRouter can identify the routing operations that these routing methods actually perform after being called, so you can now remove the interface after using these methods as well.

Supports user-defined routes

ZIKViewRouter also supports custom routing and removal routing methods in subclasses. Just write the corresponding protocol.

About the jump method in Extension

App in the extension and some special jump method, such as Watch expanding WKInterfaceController – pushControllerWithName: in the context, and – popController, Share in the extension SLComposeServiceViewController – pushConfigurationViewController: and – popConfigurationViewController.

There are more than ten types of extension, too lazy to adapt one by one. And the interface in Extension will not be particularly complex and will not require routing tools. If you need an extension, you can add it yourself, or use ZIKViewRouteTypeCustom.

Support the storyboard

The ZIKViewRouter supports storyboard, which is also better than other routers. After all, storyboards can be useful sometimes. When you use the Router in a storyboard project, you can’t reconfigure all the storyboard interfaces to accommodate the router.

The principle of adaptation storyboard is the hook all the UIViewController – prepareForSegue: sender: method, check whether destinationViewController observe ZIKRoutableView agreement, If yes, it indicates that the interface is managed by the router. The interface obtains registered router classes, generates router instances, and implements dependency injection on them. If the destination need to dynamic parameter, is called sourceViewController – prepareDestinationFromExternal: configuration: method, let sourceViewController refs. If multiple Router classes are registered with the same View Controller, select a random router.

You don’t need to make any changes to existing modules to make them compatible. And the -prepareForsegue: Sender: in the original View Controller will still work.

AOP

The ZIKViewRouter calls back four methods when routing and removing routes from an interface:

+ (void)router:(nullable ZIKViewRouter *)router willPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didPerformRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router willRemoveRouteOnDestination:(id)destination fromSource:(id)source {
}
+ (void)router:(nullable ZIKViewRouter *)router didRemoveRouteOnDestination:(id)destination fromSource:(id)source{}Copy the code

You can check that the interface is configured correctly in these methods. It can also be used for AOP logging.

For example, you can register a Router for UIViewController, the parent of all View Controllers, to monitor routing events for all UIViewController subclasses.

Route error check

ZIKRouter registers all routers during startup. In this way, the ZIKRouter can check whether the router conflicts and whether the protocol matches the router correctly to ensure that all routers work correctly. The assertion will fail when an error is detected.

ZIKViewRouter detects and reports routing errors when performing interface routing. Such as:

  • Incorrect protocol was used to perform routing
  • The configuration is incorrect during route execution. Procedure
  • Unsupported routing modes (The Router can restrict the interface to only push, present and other limited jump modes)
  • Perform a jump to another interface while jumping to another interface (unbalanced transitionMistakes can lead to-viewWillAppear:,-viewDidAppear:,-viewWillDisAppear:,-viewDidDisappear:The sequence of events is out of order)
  • The Source View Controller is not in a state to execute the current route
  • The Container View Controller is incorrectly configured during routing. Procedure
  • The segue is canceled in the proxy method, causing the route not to execute
  • Repeat routing

Basically contains most of the error events that occur when the interface jumps.

Support for any module

The ZIKRouter contains the ZIKViewRouter and ZIKServiceRouter. ZIKViewRouter is dedicated to interface jumps, and ZIKServiceRouter can add any class for instance retrieval.

You can use ZIKServiceRouter to manage the required classes, and ZIKServiceRouter adds the same dynamic and generic support as ZIKViewRouter.

performance

For error checking, storyboard support, and registration, ZIKViewRouter and ZIKServiceRouter loop through all classes at app startup, hook and register. Add the view class, Protocol, and Router class addresses to the dictionary during registration.

In release mode, 5000 UIViewControllers and 5000 corresponding routers were tested on iPhone6s model, all classes were traversed and the hook time was about 15ms, and the time to register the router was about 50ms. Performance problems are virtually non-existent.

If you don’t need to support the storyboard, you can remove the registration of the view class and router class pairing. If you remove the registration, you will not be able to automatically create routers for the View Controller in the storyboard. As for protocol and Router registration, it seems unavoidable at present.

Project address and Demo

Simply put, ZIKRouter is a Router for intermodule routing, module discovery and dependency injection based on interfaces. It performs routing in native syntax and is available in both OC and Swift.

The project address is: ZIKRouter. It includes a demo that demonstrates most of the interface routing scenarios in iOS and is recommended to run on a landscape iPad.

Finally, click star

In the Demo screenshot, the console output is the AOP callback for interface routing:

reference

  • IOS componentization solution
  • IOS componentization — Analysis of route Design
  • component-diagrams
  • BeeHive
  • CTMediator