An interface-oriented router for discovering 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.
一个用于模块间路由,基于接口进行模块发现和依赖注入的解耦工具。
View router 将 UIKit 中的所有界面跳转方式封装成一个统一的方法。
Service router 用于模块寻找,通过 protocol 寻找对应的模块,并用 protocol 进行依赖注入和模块调用。可和其他 URL router 兼容。
- Support Swift and Objective-C
- Support iOS, macOS and tvOS
- Routing for UIViewController / NSViewController, UIView / NSView and any classes
- Dependency injection
- Locate module with its protocol
- Locate module with identifier, compatible with other URL router
- Prepare the module with its protocol when performing route, rather than passing a parameter dictionary
- Declare routable protocol. There're compile-time checking and runtime checking to make reliable routing
- Use different require protocol and provided protocol inside module and module's user to make thorough decouple
- Decouple modules and add compatible interfaces with adapter
- Declare a specific router with generic parameters
- Encapsulate navigation methods in UIKit and AppKit (push, present modally, present as popover present as sheet, segue, show, showDetail, addChildViewController, addSubview) and custom transitions into one method
- Remove an UIViewController/UIView or unload a module through one method, without using pop、dismiss、removeFromParentViewController、removeFromSuperview in different situation. Router can choose the proper method
- Support storyboard. UIViewController / NSViewController and UIView / NSView from a segue can auto create it's registered router
- Error checking for view transition
- AOP for view transition
- Detect memory leaks
- Send custom events to router
- Auto register all routers, or manually register each router
- Add route with router subclasses, or with blocks
- Router Implementation
- Module Registration
- Routable Declaration
- Type Checking
- Perform Route
- Remove Route
- Make Destination
- iOS 7.0+
- Swift 3.2+
- Xcode 9.0+
Add this to your Podfile.
For Objective-C project:
pod 'ZIKRouter', '>= 1.0.6'
For Swift project:
pod 'ZRouter', '>= 1.0.6'
Add this to your Cartfile:
github "Zuikyo/ZIKRouter" >= 1.0.6
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 NoteEditorInput: class {
weak var delegate: EditorDelegate? { get set }
func constructForCreatingNewNote()
}
///Editor view controller
class NoteEditorViewController: UIViewController, NoteEditorInput {
...
}
Objective-C Sample
///editor view's interface
@protocol NoteEditorInput <ZIKViewRoutable>
@property (nonatomic, weak) id<EditorDelegate> delegate;
- (void)constructForCreatingNewNote;
@end
///Editor view controller
@interface NoteEditorViewController: UIViewController <NoteEditorInput>
@end
@implementation NoteEditorViewController
@end
There're 2 steps to create route for your module.
Create router subclass for your module:
import ZIKRouter.Internal
import ZRouter
class NoteEditorViewRouter: ZIKViewRouter<NoteEditorViewController, ViewRouteConfig> {
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<NoteEditorInput>())
}
// Return the destination module
override func destination(with configuration: ViewRouteConfig) -> NoteEditorViewController? {
let destination: NoteEditorViewController? = ... /// instantiate your view controller
return destination
}
override func prepareDestination(_ destination: NoteEditorViewController, configuration: ViewRouteConfig) {
// Inject dependencies to destination
}
}
Objective-C Sample
//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(NoteEditorInput)];
}
// Return the destination module
- (NoteEditorViewController *)destinationWithConfiguration:(ZIKViewRouteConfiguration *)configuration {
NoteEditorViewController *destination = ... /// instantiate your view controller
return destination;
}
- (void)prepareDestination:(NoteEditorViewController *)destination configuration:(ZIKViewRouteConfiguration *)configuration {
// Inject dependencies to destination
}
@end
Read the documentation for more details and more methods to override.
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 NoteEditorInput is routable
// This means you can use NoteEditorInput to fetch router
// If you use an undeclared protocol, there will be compile time error
extension RoutableView where Protocol == NoteEditorInput {
init() { self.init(declaredProtocol: Protocol.self) }
}
Objective-C Sample
// 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 NoteEditorInput to fetch router
// If you use an undeclared protocol, there will be compile time warning
@protocol NoteEditorInput <ZIKViewRoutable>
@property (nonatomic, weak) id<EditorDelegate> delegate;
- (void)constructForCreatingNewNote;
@end
Now you can get and show NoteEditorViewController
with router.
Transition to editor view directly:
class TestViewController: UIViewController {
//Transition to editor view directly
func showEditorDirectly() {
Router.perform(to: RoutableView<NoteEditorInput>(), path: .push(from: self))
}
}
Objective-C Sample
@implementation TestViewController
- (void)showEditorDirectly {
//Transition to editor view directly
[ZIKRouterToView(NoteEditorInput) 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)
}
Transition to editor view, and prepare it before transition:
class TestViewController: UIViewController {
//Transition to editor view, and prepare the destination with NoteEditorInput
func showEditor() {
Router.perform(
to: RoutableView<NoteEditorInput>(),
path: .push(from: self),
configuring: { (config, _) in
//Route config
config.successHandler = { destination in
//Transition succeed
}
config.errorHandler = { (action, error) in
//Transition failed
}
//Prepare the destination before transition
config.prepareDestination = { [weak self] destination in
//destination is inferred as NoteEditorInput
destination.delegate = self
destination.constructForCreatingNewNote()
}
})
}
}
Objective-C Sample
@implementation TestViewController
- (void)showEditor {
//Transition to editor view, and prepare the destination with NoteEditorInput
[ZIKRouterToView(NoteEditorInput)
performPath:ZIKViewRoutePath.pushFrom(self)
configuring:^(ZIKViewRouteConfig *config) {
//Route config
//Prepare the destination before transition
config.prepareDestination = ^(id<NoteEditorInput> destination) {
destination.delegate = self;
[destination constructForCreatingNewNote];
};
config.successHandler = ^(id<NoteEditorInput> destination) {
//Transition is completed
};
config.errorHandler = ^(ZIKRouteAction routeAction, NSError * error) {
//Transition failed
};
}];
}
@end
For more detail, read Perform Route.
You can remove the view by removeRoute
, without using pop / dismiss / removeFromParentViewController / removeFromSuperview:
class TestViewController: UIViewController {
var router: DestinationViewRouter<NoteEditorInput>?
func showEditor() {
//Hold the router
router = Router.perform(to: RoutableView<NoteEditorInput>(), 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
}
}
Objective-C Sample
@interface TestViewController()
@property (nonatomic, strong) ZIKDestinationViewRouter(id<NoteEditorInput>) *router;
@end
@implementation TestViewController
- (void)showEditorDirectly {
//Hold the router
self.router = [ZIKRouterToView(NoteEditorInput) 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<NoteEditorInput> *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 RequiredNoteEditorInput: class {
weak var delegate: EditorDelegate? { get set }
func constructForCreatingNewNote()
}
Objective-C Sample
///Required protocol to use editor module
@protocol RequiredNoteEditorInput <ZIKViewRoutable>
@property (nonatomic, weak) id<EditorDelegate> delegate;
- (void)constructForCreatingNewNote;
@end
UseRequiredNoteEditorInput
to get module:
class TestViewController: UIViewController {
func showEditorDirectly() {
Router.perform(to: RoutableView<RequiredNoteEditorInput>(), path: .push(from: self))
}
}
Objective-C Sample
@implementation TestViewController
- (void)showEditorDirectly {
[ZIKRouterToView(RequiredNoteEditorInput) performPath:ZIKViewRoutePath.pushFrom(self)];
}
@end
Use required protocol
and provided protocol
to 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.
You need to connect required protocol and provided protocol. For more detail, read Module Adapter.
ZIKRouter is also compatible with other URL router frameworks.
You can register string identifier with router:
class NoteEditorViewRouter: ZIKViewRouter<NoteEditorViewController, ViewRouteConfig> {
override class func registerRoutableDestination() {
// Register identifier with this router
registerIdentifier("myapp://noteEditor")
}
}
Objective-C Sample
@implementation NoteEditorViewRouter
+ (void)registerRoutableDestination {
// Register identifier with this router
[self registerIdentifier:@"myapp://noteEditor"];
}
@end
Then perform route with the identifier:
Router.to(viewIdentifier: "myapp://noteEditor")?.perform(path .push(from: self))
Objective-C Sample
[ZIKViewRouter.toIdentifier(@"myapp://noteEditor") performPath:ZIKViewRoutePath.pushFrom(self)];
And handle URL Scheme:
public func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
// You can use other URL router frameworks
let routerIdentifier = URLRouter.routerIdentifierFromURL(url)
guard let identifier = routerIdentifier else {
return false
}
guard let routerType = Router.to(viewIdentifier: identifier) else {
return false
}
let params: [String : Any] = [ "url": url, "options": options ]
routerType.perform(path: .show(from: rootViewController), configuring: { (config, _) in
// Pass parameters
config.addUserInfo(params)
})
return true
}
Objective-C Sample
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
// You can use other URL router frameworks
NSString *identifier = [URLRouter routerIdentifierFromURL:url];
if (identifier == nil) {
return NO;
}
ZIKViewRouterType *routerType = ZIKViewRouter.toIdentifier(identifier);
if (routerType == nil) {
return NO;
}
NSDictionary *params = @{ @"url": url,
@"options" : options
};
[routerType performPath:ZIKViewRoutePath.showFrom(self.rootViewController)
configuring:^(ZIKViewRouteConfiguration * _Nonnull config) {
// Pass parameters
[config addUserInfo:params];
}];
return YES;
}
If you don't wan't to show a view, but only need to get instance of the module, you can use makeDestination
:
let destination = Router.makeDestination(to: RoutableView<NoteEditorInput>())
Objective-C Sample
id<NoteEditorInput> destination = [ZIKRouterToView(NoteEditorInput) makeDestination];
And you can also get any custom 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()
}
}
Objective-C Sample
///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.
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.xctemplate
is in Templates.
Copy ZIKRouter.xctemplate
to ~/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.