Interface-oriented router for discovering modules, and injecting dependencies with protocol in Objective-C and Swift.
An interface-oriented router for managing modules and injecting dependencies with protocol.
The view router can perform all navigation types in UIKit / AppKit through one method.
The service router can discover and prepare corresponding module with its protocol.
一个用于模块间解耦和通信,基于接口进行模块管理和依赖注入的组件化路由工具。用多种方式最大程度地发挥编译检查的功能。
通过 protocol 寻找对应的模块,并用 protocol 进行依赖注入和模块通信。
Service Router 可以管理任意自定义模块。View Router 进一步封装了界面跳转。
Add this to your Podfile.
For Objective-C project:
pod 'ZIKRouter', '>= 1.1.1'or only use ServiceRouter
pod 'ZIKRouter/ServiceRouter' , '>=1.1.1'
For Swift project:
pod 'ZRouter', '>= 1.1.1'or only use ServiceRouter
pod 'ZRouter/ServiceRouter' , '>=1.1.1'
Add this to your Cartfile:
github "Zuikyo/ZIKRouter" >= 1.1.1
Build frameworks:
carthage update
Build DEBUG version to enable route checking:
carthage update --configuration Debug
Remember to use release version in production environment.
For Objective-C project, use
ZIKRouter.framework. For Swift project, use
ZRouter.framework.
This is the demo view controller and protocol:
///Editor view's interface protocol EditorViewInput: class { weak var delegate: EditorDelegate? { get set } func constructForCreatingNewNote() }///Editor view controller class NoteEditorViewController: UIViewController, EditorViewInput { ... }
///editor view's interface
@protocol EditorViewInput <zikviewroutable>
@property (nonatomic, weak) id<editordelegate> delegate;
- (void)constructForCreatingNewNote;
@end
///Editor view controller
@interface NoteEditorViewController: UIViewController <editorviewinput>
@end
@implementation NoteEditorViewController
@end
There're 2 steps to create route for your module.
To make your class become modular, you need to create router for your module. You don't need to modify the module's code. That will reduce the cost for refactoring existing modules.
Create router subclass for your module:
import ZIKRouter.Internal import ZRouterclass NoteEditorViewRouter: ZIKViewRouter { override class func registerRoutableDestination() { // Register class with this router. A router can register multi views, and a view can be registered with multi routers registerView(NoteEditorViewController.self) // Register protocol. Then we can fetch this router with the protocol register(RoutableView()) }
// Return the destination module override func destination(with configuration: ViewRouteConfig) -> NoteEditorViewController? { // In configuration, you can get parameters from the caller for creating the instance let destination: NoteEditorViewController? = ... /// instantiate your view controller return destination } override func prepareDestination(_ destination: NoteEditorViewController, configuration: ViewRouteConfig) { // Inject dependencies to destination }
}
//NoteEditorViewRouter.h
@import ZIKRouter;
@interface NoteEditorViewRouter : ZIKViewRouter
@end
//NoteEditorViewRouter.m
@import ZIKRouter.Internal;
@implementation NoteEditorViewRouter
+ (void)registerRoutableDestination {
// Register class with this router. A router can register multi views, and a view can be registered with multi routers
[self registerView:[NoteEditorViewController class]];
// Register protocol. Then we can fetch this router with the protocol
[self registerViewProtocol:ZIKRoutable(EditorViewInput)];
}
// Return the destination module
- (NoteEditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
// In configuration, you can get parameters from the caller for creating the instance
NoteEditorViewController *destination = ... // instantiate your view controller
return destination;
}
- (void)prepareDestination:(NoteEditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
// Inject dependencies to destination
}
@end
Each router can control their own routing, such as using different custom transition. And the router can be very easy to add additional features.
Read the documentation for more details and more methods to override.
If your module is very simple and don't need a router subclass, you can just register the class in a simpler way:
ZIKAnyViewRouter.register(RoutableView(), forMakingView: NoteEditorViewController.self)
[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewInput) forMakingView:[NoteEditorViewController class]];
or with custom creating block:
ZIKAnyViewRouter.register(RoutableView(), forMakingView: NoteEditorViewController.self) { (config, router) -> EditorViewInput? in let destination: NoteEditorViewController? = ... // instantiate your view controller return destination; }
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewInput)
forMakingView:[NoteEditorViewController class]
making:^id _Nullable(ZIKViewRouteConfiguration *config, ZIKViewRouter *router) {
NoteEditorViewController *destination = ... // instantiate your view controller
return destination;
}];
or with custom factory function:
function makeEditorViewController(config: ViewRouteConfig) -> EditorViewInput? { let destination: NoteEditorViewController? = ... // instantiate your view controller return destination; }ZIKAnyViewRouter.register(RoutableView(), forMakingView: NoteEditorViewController.self, making: makeEditorViewController)
id<editorviewinput> makeEditorViewController(ZIKViewRouteConfiguration *config) {
NoteEditorViewController *destination = ... // instantiate your view controller
return destination;
}
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewInput)
forMakingView:[NoteEditorViewController class]
factory:makeEditorViewController];
The declaration is for checking routes at compile time, and supporting storyboard.
// Declare NoteEditorViewController is routable // This means there is a router for NoteEditorViewController extension NoteEditorViewController: ZIKRoutableView { }// Declare EditorViewInput is routable // This means you can use EditorViewInput to fetch router extension RoutableView where Protocol == EditorViewInput { init() { self.init(declaredProtocol: Protocol.self) } }
// Declare NoteEditorViewController is routable
// This means there is a router for NoteEditorViewController
DeclareRoutableView(NoteEditorViewController, NoteEditorViewRouter)
// If the protocol inherits from ZIKViewRoutable, it's routable
// This means you can use EditorViewInput to fetch router
@protocol EditorViewInput <zikviewroutable>
@property (nonatomic, weak) id<editordelegate> delegate;
- (void)constructForCreatingNewNote;
@end
If you use an undeclared protocol for routing, there will be compile time error. So it's much safer and easier to manage protocols and to know which protocols are routable.
Unroutable error in Swift:
Unroutable error in Objective-C:
Now you can get and show
NoteEditorViewControllerwith router.
Transition to editor view directly:
class TestViewController: UIViewController {// Transition to editor view directly func showEditorDirectly() { Router.perform(to: RoutableView<editorviewinput>(), path: .push(from: self)) }
}
@implementation TestViewController
- (void)showEditorDirectly {
// Transition to editor view directly
[ZIKRouterToView(EditorViewInput) performPath:ZIKViewRoutePath.pushFrom(self)];
}
@end
You can change transition type with
ViewRoutePath:
enum ViewRoutePath { case push(from: UIViewController) case presentModally(from: UIViewController) case presentAsPopover(from: UIViewController, configure: ZIKViewRoutePopoverConfigure) case performSegue(from: UIViewController, identifier: String, sender: Any?) case show(from: UIViewController) case showDetail(from: UIViewController) case addAsChildViewController(from: UIViewController, addingChildViewHandler: (UIViewController, @escaping () -> Void) -> Void) case addAsSubview(from: UIView) case custom(from: ZIKViewRouteSource?) case makeDestination case extensible(path: ZIKViewRoutePath) }
Encapsulating view transition can hide the UIKit detail, then you can perform route outside the view layer (presenter, view model, interactor, service) and be cross-platform.
Prepare it before transition to editor view:
class TestViewController: UIViewController {// Transition to editor view, and prepare the destination with EditorViewInput func showEditor() { Router.perform( to: RoutableView<editorviewinput>(), path: .push(from: self), configuring: { (config, _) in // Route config // Prepare the destination before transition config.prepareDestination = { [weak self] destination in //destination is inferred as EditorViewInput destination.delegate = self destination.constructForCreatingNewNote() } config.successHandler = { destination in // Transition succeed } config.errorHandler = { (action, error) in // Transition failed } }) }
}
@implementation TestViewController
- (void)showEditor {
// Transition to editor view, and prepare the destination with EditorViewInput
[ZIKRouterToView(EditorViewInput)
performPath:ZIKViewRoutePath.pushFrom(self)
configuring:^(ZIKViewRouteConfig *config) {
// Route config
// Prepare the destination before transition
config.prepareDestination = ^(id<editorviewinput> destination) {
destination.delegate = self;
[destination constructForCreatingNewNote];
};
config.successHandler = ^(id<editorviewinput> destination) {
// Transition is completed
};
config.errorHandler = ^(ZIKRouteAction routeAction, NSError * error) {
// Transition failed
};
}];
}
@end
For more detail, read Perform Route.
If you don't want to show a view, but only need to get instance of the module, you can use
makeDestination:
// destination is inferred as EditorViewInput let destination = Router.makeDestination(to: RoutableView())
id<editorviewinput> destination = [ZIKRouterToView(EditorViewInput) makeDestination];
Some parameters can't be delivered though destination's protocol:
the destination class uses custom initializers to create instance, router needs to get required parameter from the caller
the module contains multi components, and you need to pass parameters to those components. Those parameters do not belong to the destination, so they should not exist in destination's protocol
You can use module config protocol and a custom configuration to transfer parameters.
Instead of
EditorViewInput, we use another routable protocol
EditorViewModuleInputas config protocol for routing:
// In general, a module config protocol only contains `makeDestinationWith`, for declaring parameters and destination type. You can also add other properties or methods protocol EditorViewModuleInput: class { // Factory method for transferring parameters and making destination var makeDestinationWith: (_ note: Note) -> EditorViewInput? { get } }
// In general, a module config protocol only contains `makeDestinationWith`, for declaring parameters and destination type. You can also add other properties or methods
@protocol EditorViewModuleInput <zikviewmoduleroutable>
// Factory method for transferring parameters and making destination
@property (nonatomic, copy, readonly) id<editorviewinput> _Nullable(^makeDestinationWith)(Note *note);
@end
This configuration works like a factory for the destination with
EditorViewModuleInputprotocol. It declares parameters for creating the destination.
Now the user can use the module with its module config protocol and transfer parameters:
var note = ... Router.makeDestination(to: RoutableViewModule()) { (config) in // Transfer parameters and get EditorViewInput let destination = config.makeDestinationWith(note) }
Note *note = ...
[ZIKRouterToViewModule(EditorViewModuleInput)
performPath:ZIKViewRoutePath.showFrom(self)
configuring:^(ZIKViewRouteConfiguration<editorviewmoduleinput> *config) {
// Transfer parameters and get EditorViewInput
id<editorviewinput> destination = config.makeDestinationWith(note);
}];
For more detail, read Transfer Parameters with Custom Configuration.
If you get a destination from other place, you can perform on the destination with its router.
For example, an UIViewController supports 3D touch, and implments
UIViewControllerPreviewingDelegate:
class SourceViewController: UIViewController, UIViewControllerPreviewingDelegate { func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController? { // Return the destination UIViewController to let system preview it let destination = Router.makeDestination(to: RoutableView()) return destination }func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController) { guard let destination = viewControllerToCommit as? EditorViewInput else { return } // Show the destination Router.to(RoutableView<editorviewinput>())?.perform(onDestination: destination, path: .presentModally(from: self))
}
@implementation SourceViewController
- (nullable UIViewController *)previewingContext:(id <uiviewcontrollerpreviewing>)previewingContext viewControllerForLocation:(CGPoint)location {
//Return the destination UIViewController to let system preview it
UIViewController<editorviewinput> *destination = [ZIKRouterToView(EditorViewInput) makeDestination];
return destination;
}
- (void)previewingContext:(id <uiviewcontrollerpreviewing>)previewingContext commitViewController:(UIViewController *)viewControllerToCommit {
// Show the destination
UIViewController<editorviewinput> *destination;
if ([viewControllerToCommit conformsToProtocol:@protocol(EditorViewInput)]) {
destination = viewControllerToCommit;
} else {
return;
}
[ZIKRouterToView(EditorViewInput) performOnDestination:destination path:ZIKViewRoutePath.presentModallyFrom(self)];
}
@end
If you don't want to show the destination, but just want to prepare an existing destination, you can prepare the destination with its router.
If the router injects dependencies inside it, this can properly setting the destination instance.
var destination: DestinationViewInput = ... Router.to(RoutableView())?.prepare(destination: destination, configuring: { (config, _) in config.prepareDestination = { destination in // Prepare } })
UIViewController<editorviewinput> *destination = ...
[ZIKRouterToView(EditorViewInput) prepareDestination:destination configuring:^(ZIKViewRouteConfiguration *config) {
config.prepareDestination = ^(id<editorviewinput> destination) {
// Prepare
};
}];
You can remove the view by
removeRoute, without using pop / dismiss / removeFromParentViewController / removeFromSuperview:
class TestViewController: UIViewController { var router: DestinationViewRouter?func showEditor() { // Hold the router router = Router.perform(to: RoutableView<editorviewinput>(), path: .push(from: self)) } // Router will pop the editor view controller func removeEditorDirectly() { guard let router = router, router.canRemove else { return } router.removeRoute() router = nil } func removeEditorWithResult() { guard let router = router, router.canRemove else { return } router.removeRoute(successHandler: { print("remove success") }, errorHandler: { (action, error) in print("remove failed, error: \(error)") }) router = nil } func removeEditorAndPrepare() { guard let router = router, router.canRemove else { return } router.removeRoute(configuring: { (config) in config.animated = true config.prepareDestination = { destination in // Use destination before remove it } }) router = nil }
}
@interface TestViewController()
@property (nonatomic, strong) ZIKDestinationViewRouter(id<editorviewinput>) *router;
@end
@implementation TestViewController
- (void)showEditorDirectly {
// Hold the router
self.router = [ZIKRouterToView(EditorViewInput) performPath:ZIKViewRoutePath.pushFrom(self)];
}
// Router will pop the editor view controller
- (void)removeEditorDirectly {
if (![self.router canRemove]) {
return;
}
[self.router removeRoute];
self.router = nil;
}
- (void)removeEditorWithResult {
if (![self.router canRemove]) {
return;
}
[self.router removeRouteWithSuccessHandler:^{
NSLog(@"pop success");
} errorHandler:^(ZIKRouteAction routeAction, NSError *error) {
NSLog(@"pop failed,error: %@",error);
}];
self.router = nil;
}
- (void)removeEditorAndPrepare {
if (![self.router canRemove]) {
return;
}
[self.router removeRouteWithConfiguring:^(ZIKViewRemoveConfiguration *config) {
config.animated = YES;
config.prepareDestination = ^(UIViewController<editorviewinput> *destination) {
// Use destination before remove it
};
}];
self.router = nil;
}
@end
For more detail, read Remove Route.
You can use another protocol to get router, as long as the protocol provides the same interface of the real protocol. Even the protocol is little different from the real protocol, you can adapt two protocols with category, extension and proxy.
Required protocol used by the user:
/// Required protocol to use editor module protocol RequiredEditorViewInput: class { weak var delegate: EditorDelegate? { get set } func constructForCreatingNewNote() }
/// Required protocol to use editor module
@protocol RequiredEditorViewInput <zikviewroutable>
@property (nonatomic, weak) id<editordelegate> delegate;
- (void)constructForCreatingNewNote;
@end
In the host app context, connect required protocol and provided protocol: ```swift /// In the host app, add required protocol to editor router class EditorViewAdapter: ZIKViewRouteAdapter { override class func registerRoutableDestination() { // If you can get the router, you can just register RequiredEditorViewInput to it NoteEditorViewRouter.register(RoutableView())
// If you don't know the router, you can use adapter register(adapter: RoutableView(), forAdaptee: RoutableView()) }
}
/// Make NoteEditorViewController conform to RequiredEditorViewInput extension NoteEditorViewController: RequiredEditorViewInput { } ```
/// In the host app, add required protocol to editor router
//EditorViewAdapter.h
@interface EditorViewAdapter : ZIKViewRouteAdapter
@end
//EditorViewAdapter.m
@implementation EditorViewAdapter
+ (void)registerRoutableDestination {
// If you can get the router, you can just register RequiredEditorViewInput to it
[NoteEditorViewRouter registerViewProtocol:ZIKRoutable(RequiredEditorViewInput)];
// If you don't know the router, you can use adapter
[self registerDestinationAdapter:ZIKRoutable(RequiredEditorViewInput) forAdaptee:ZIKRoutable(EditorViewInput)];
}
@end
/// Make NoteEditorViewController conform to RequiredEditorViewInput
@interface NoteEditorViewController (Adapter) <requirededitorviewinput>
@end
@implementation NoteEditorViewController (Adapter)
@end
After adapting,
RequiredEditorViewInputand
EditorViewInputcan get the same router.
Use
RequiredEditorViewInputto get module:
class TestViewController: UIViewController {func showEditorDirectly() { Router.perform(to: RoutableView<requirededitorviewinput>(), path: .push(from: self)) }
}
@implementation TestViewController
- (void)showEditorDirectly {
[ZIKRouterToView(RequiredEditorViewInput) performPath:ZIKViewRoutePath.pushFrom(self)];
}
@end
Use
required protocoland
provided protocolto perfectly decouple modules, adapt interface and declare dependencies of the module. And you don't have to use a public header to manage those protocols.
Separating
required protocoland
provided protocolmakes your code truly modular. The caller declares its
required protocol, and the provided module can easily be replaced by another module with the same
required protocol.
Read the
ZIKLoginModulemodule in demo. The login module depends on an alert module, and the alert module is different in
ZIKRouterDemoand
ZIKRouterDemo-macOS. You can change the provided module without changing anything in the login module.
For more detail, read Module Adapter.
ZIKRouter also provides a default URLRouter. It's easy to communicate with modules via url.
URLRouter is not contained by default. If you want to use it, add submodule
pod 'ZIKRouter/URLRouter'to your
Podfile, and call
[ZIKRouter enableDefaultURLRouteRule]to enable URLRouter.
You can register router with a url:
class NoteEditorViewRouter: ZIKViewRouter { override class func registerRoutableDestination() { registerView(NoteEditorViewController.self) register(RoutableView()) // Register url registerURLPattern("app://editor/:title") } }
@implementation NoteEditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[NoteEditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewInput)];
// Register url
[self registerURLPattern:@"app://editor/:title"];
}
@end
Then you can get the router with it's url:
ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))
[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];
And handle URL Scheme:
public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { let urlString = url.absoluteString if let _ = ZIKAnyViewRouter.performURL(urlString, fromSource: self.rootViewController) { return true } else if let _ = ZIKAnyServiceRouter.performURL(urlString) { return true } else { return false } }
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<uiapplicationopenurloptionskey> *)options {
if ([ZIKAnyViewRouter performURL:urlString fromSource:self.rootViewController]) {
return YES;
} else if ([ZIKAnyServiceRouter performURL:urlString]) {
return YES;
} else {
return NO;
}
}
If your project has different requirements for URL router, you can write your URL router by yourself. You can create custom ZIKRouter as parent class, add more powerful features in it. See
ZIKRouter+URLRouter.h.
There're other features, you can get details in the documentation:
Instead of view, you can also get any service modules:
/// time service's interface protocol TimeServiceInput { func currentTimeString() -> String }
class TestViewController: UIViewController { @IBOutlet weak var timeLabel: UILabel!func callTimeService() { // Get the service for TimeServiceInput let timeService = Router.makeDestination( to: RoutableService<timeserviceinput>(), preparation: { destination in // prepare the service if needed }) //Use the service timeLabel.text = timeService.currentTimeString() }
}
/// time service's interface
@protocol TimeServiceInput <zikserviceroutable>
- (NSString *)currentTimeString;
@end
@interface TestViewController ()
@property (weak, nonatomic) IBOutlet UILabel *timeLabel;
@end
@implementation TestViewController
- (void)callTimeService {
// Get the service for TimeServiceInput
id<timeserviceinput> timeService = [ZIKRouterToService(TimeServiceInput) makeDestination];
self.timeLabel.text = [timeService currentTimeString];
}
ZIKRouter is designed for VIPER architecture at first. But you can also use it in MVC or anywhere.
The demo (ZIKRouterDemo) in this repository shows how to use ZIKRouter to perform each route type. Open
Router.xcworkspaceto run it.
If you want to see how it works in a VIPER architecture app, go to ZIKViper.
You can use Xcode file template to create router and protocol code quickly:
The template
ZIKRouter.xctemplateis in Templates.
Copy
ZIKRouter.xctemplateto
~/Library/Developer/Xcode/Templates/ZIKRouter.xctemplate, then you can use it in
Xcode -> File -> New -> File -> Templates.
ZIKRouter is available under the MIT license. See the LICENSE file for more info.