ZIKRouter
一个针对模块管理和通过协议进行依赖注入的接口导向路由器。
视图路由器可以通過一个方法执行 UIKit / AppKit 中所有导航类型。
服务路由器可以寻找到对应的模块并准备它,通过其协议。
这是一个用于模块解耦和通信的组件化路由工具,基于接口进行模块管理和依赖注入。通过多种方式最大限度地发挥编译检查的功能。
通过 protocol 寻找对应的模块,通过 protocol 进行依赖注入和模块通信。
服务路由器可以管理任意自定义模块。视图路由器封装了界面跳转。
中文文档
特性
- 支持 Swift 和 Objective-C
- 支持 iOS, macOS 和 tvOS
- 创建路由器的文件模板
- 支持 UIViewController / NSViewController, UIView / NSView 以及任何类的路由
- 支持依赖注入,包括动态注入和静态注入
- 通过声明可路由协议进行编译时检查。使用未声明的协议将会带来编译错误。这是最强大的特性之一
- 通过其协议与模块进行匹配
- 支持 URL 路由
- 通过其协议配置模块,而不是通过参数字典
- 需要和提供的协议用于彻底解耦
- 适配器用于解耦模块并添加兼容接口
- 支持 Storyboard。可以通过 segue 自动准备视图
- 封装 UIKit / AppKit 中所有的过渡方法和 unwind 方法,以及自定义过渡
- 对视图过渡进行检查
- 对视图过渡支持 AOP
- 内存泄漏检测
- 自定义事件处理
- 自动注册
- 高度可扩展
快速入门指南
文档
设计理念
基础
高级特性
要求
- iOS 7.0+
- Swift 3.2+
- Xcode 9.0+
安装
Cocoapods
将此内容添加到您的Podfile中。
针对Objective-C项目
pod 'ZIKRouter', '>= 1.1.1'
# or only use ServiceRouter
pod 'ZIKRouter/ServiceRouter' , '>=1.1.1'
针对Swift项目
pod 'ZRouter', '>= 1.1.1'
# or only use ServiceRouter
pod 'ZRouter/ServiceRouter' , '>=1.1.1'
Carthage
将此内容添加到您的Cartfile。
github "Zuikyo/ZIKRouter" >= 1.1.1
构建框架
carthage update
构建DEBUG版本以启用路由检查
carthage update --configuration Debug
请记住在生产环境中使用发布版本。
对于Objective-C项目,使用 ZIKRouter.framework
。对于Swift项目,使用 ZRouter.framework
。
入门
这是一个示例视图控制器和协议:
///Editor view's interface
protocol EditorViewInput: class {
weak var delegate: EditorDelegate? { get set }
func constructForCreatingNewNote()
}
///Editor view controller
class NoteEditorViewController: UIViewController, EditorViewInput {
...
}
Objective-C 示例
///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
创建您的模块路由有两个步骤。
1. 创建路由
为了使您的类成为模块化,您需要为您的模块创建路由。您不需要修改模块的代码。这将会减少现有模块重构的成本。
1.1 路由子类
为您的模块创建路由子类
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<EditorViewInput>())
}
// 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
}
}
Objective-C 示例
//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
每个路由可以控制自己的路由,例如使用不同的自定义过渡。并且路由可以非常容易地添加附加功能。
阅读更多文档以获取详细信息以及更多的覆盖方法。
1.2 简单路由
如果您的模块非常简单且不需要路由子类,您只需以更简单的方式注册该类
ZIKAnyViewRouter.register(RoutableView<EditorViewInput>(), forMakingView: NoteEditorViewController.self)
Objective-C 示例
[ZIKViewRouter registerViewProtocol:ZIKRoutable(EditorViewInput) forMakingView:[NoteEditorViewController class]];
或者通过自定义创建块
ZIKAnyViewRouter.register(RoutableView<EditorViewInput>(),
forMakingView: NoteEditorViewController.self) { (config, router) -> EditorViewInput? in
let destination: NoteEditorViewController? = ... // instantiate your view controller
return destination;
}
Objective-C 示例
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewInput)
forMakingView:[NoteEditorViewController class]
making:^id _Nullable(ZIKViewRouteConfiguration *config, ZIKViewRouter *router) {
NoteEditorViewController *destination = ... // instantiate your view controller
return destination;
}];
或者通过自定义工厂函数
function makeEditorViewController(config: ViewRouteConfig) -> EditorViewInput? {
let destination: NoteEditorViewController? = ... // instantiate your view controller
return destination;
}
ZIKAnyViewRouter.register(RoutableView<EditorViewInput>(),
forMakingView: NoteEditorViewController.self, making: makeEditorViewController)
Objective-C 示例
id<EditorViewInput> makeEditorViewController(ZIKViewRouteConfiguration *config) {
NoteEditorViewController *destination = ... // instantiate your view controller
return destination;
}
[ZIKViewRouter
registerViewProtocol:ZIKRoutable(EditorViewInput)
forMakingView:[NoteEditorViewController class]
factory:makeEditorViewController];
2. 声明可路由类型
声明用于在编译时检查路由,并支持故事板。
// 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) }
}
Objective-C 示例
// 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
如果您使用未声明的协议进行路由,则会发生编译时错误。因此,管理协议和了解哪些协议是可路由的更加安全且易于管理。
Swift中的不可路由错误
Objective-C中的不可路由错误
现在您可以使用路由获取并显示NoteEditorViewController
。
视图路由器
直接过渡
直接过渡到编辑视图
class TestViewController: UIViewController {
// Transition to editor view directly
func showEditorDirectly() {
Router.perform(to: RoutableView<EditorViewInput>(), path: .push(from: self))
}
}
Objective-C 示例
@implementation TestViewController
- (void)showEditorDirectly {
// Transition to editor view directly
[ZIKRouterToView(EditorViewInput) performPath:ZIKViewRoutePath.pushFrom(self)];
}
@end
您可以通过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)
}
封装视图过渡可以隐藏UIKit的细节,然后您可以在视图层之外(演示者、视图模型、交互者、服务)执行路由,并且是跨平台的。
在转换之前准备
在转换到编辑视图之前准备它
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
}
})
}
}
Objective-C 示例
@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
更多详细信息,请参阅执行路线。
创建目标
如果你不想显示视图,只需要获取模块的实例,可以使用makeDestination
// destination is inferred as EditorViewInput
let destination = Router.makeDestination(to: RoutableView<EditorViewInput>())
Objective-C 示例
id<EditorViewInput> destination = [ZIKRouterToView(EditorViewInput) makeDestination];
必需参数和特殊参数
一些参数无法通过目标协议传递
-
目标类使用自定义初始化器来创建实例,路由器需要从调用者获取必需参数
-
模块包含多个组件,你需要将这些参数传递给这些组件。这些参数不属于目标,因此不应存在于目标的协议中
您可以使用模块配置协议和自定义配置来传递参数。
我们不使用EditorViewInput
,而使用另一个可路由协议EditorViewModuleInput
作为路由配置协议
// 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 }
}
Objective-C 示例
// 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
此配置类似于具有EditorViewModuleInput
协议的目标的工厂。它声明了用于创建目标的参数。
现在用户可以使用模块及其模块配置协议来传输参数
var note = ...
Router.makeDestination(to: RoutableViewModule<EditorViewModuleInput>()) { (config) in
// Transfer parameters and get EditorViewInput
let destination = config.makeDestinationWith(note)
}
Objective-C 示例
Note *note = ...
[ZIKRouterToViewModule(EditorViewModuleInput)
performPath:ZIKViewRoutePath.showFrom(self)
configuring:^(ZIKViewRouteConfiguration<EditorViewModuleInput> *config) {
// Transfer parameters and get EditorViewInput
id<EditorViewInput> destination = config.makeDestinationWith(note);
}];
更多详细信息,请参阅使用自定义配置传输参数。
在目标上执行
如果你从其他地方获取了目标,你可以使用其路由器在目标上执行。
例如,UIViewController支持3D touch,并实现了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<EditorViewInput>())
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))
}
Objective-C 示例
@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
在目的地准备
如果您不想显示目的地,但只想准备一个现有的目的地,您可以使用它的路由器来准备该目的地。
如果路由器在其内部注入依赖项,则可以正确配置目的地实例。
var destination: DestinationViewInput = ...
Router.to(RoutableView<EditorViewInput>())?.prepare(destination: destination, configuring: { (config, _) in
config.prepareDestination = { destination in
// Prepare
}
})
Objective-C 示例
UIViewController<EditorViewInput> *destination = ...
[ZIKRouterToView(EditorViewInput) prepareDestination:destination configuring:^(ZIKViewRouteConfiguration *config) {
config.prepareDestination = ^(id<EditorViewInput> destination) {
// Prepare
};
}];
删除
您可以通过 removeRoute
删除视图,而无需使用 pop / dismiss / removeFromParentViewController / removeFromSuperview。
class TestViewController: UIViewController {
var router: DestinationViewRouter<EditorViewInput>?
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
}
}
Objective-C 示例
@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
有关更多详细信息,请阅读 删除路由。
适配器
只要协议提供了与实际协议相同的接口,您就可以使用另一个协议来获取路由器。即使协议与实际协议略有不同,您也可以通过分类、扩展和代理来适配两个协议。
用户使用的所需协议
/// Required protocol to use editor module
protocol RequiredEditorViewInput: class {
weak var delegate: EditorDelegate? { get set }
func constructForCreatingNewNote()
}
Objective-C 示例
/// Required protocol to use editor module
@protocol RequiredEditorViewInput <ZIKViewRoutable>
@property (nonatomic, weak) id<EditorDelegate> delegate;
- (void)constructForCreatingNewNote;
@end
在宿主应用程序上下文中,连接所需的协议和提供的协议
/// 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<RequiredEditorViewInput>())
// If you don't know the router, you can use adapter
register(adapter: RoutableView<RequiredEditorViewInput>(), forAdaptee: RoutableView<EditorViewInput>())
}
}
/// Make NoteEditorViewController conform to RequiredEditorViewInput
extension NoteEditorViewController: RequiredEditorViewInput {
}
Objective-C 示例
/// 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
适应后,RequiredEditorViewInput
和 EditorViewInput
可以获取相同的路由器。
使用 RequiredEditorViewInput
获取模块
class TestViewController: UIViewController {
func showEditorDirectly() {
Router.perform(to: RoutableView<RequiredEditorViewInput>(), path: .push(from: self))
}
}
Objective-C 示例
@implementation TestViewController
- (void)showEditorDirectly {
[ZIKRouterToView(RequiredEditorViewInput) performPath:ZIKViewRoutePath.pushFrom(self)];
}
@end
使用 required 协议
和 provided 协议
完美地解耦模块,适配接口并声明模块的依赖关系。而且您无需使用公共头文件来管理这些协议。
模块化
分隔 required 协议
和 provided 协议
使您的代码更加模块化。调用者声明其 required 协议
,而提供的模块可以很容易地被具有相同 required 协议
的另一个模块替换。
查看示例中的 ZIKLoginModule
模块。登录模块依赖于一个警报模块,而警报模块在 ZIKRouterDemo
和 ZIKRouterDemo-macOS
中不同。您可以更改提供的模块,而不需要更改登录模块中的任何内容。
有关更多详细信息,请阅读 模块适配器。
URL 路由器
ZIKRouter 还提供了一个默认的 URL 路由器。它通过 URL 与模块通信很容易。
URL 路由器默认不包含在其中。如果你想使用它,请在你的 Podfile
中添加子模块 pod 'ZIKRouter/URLRouter'
,并通过调用 [ZIKRouter enableDefaultURLRouteRule]
来启用 URL 路由器。
你可以使用 URL 来注册路由器
class NoteEditorViewRouter: ZIKViewRouter<NoteEditorViewController, ViewRouteConfig> {
override class func registerRoutableDestination() {
registerView(NoteEditorViewController.self)
register(RoutableView<EditorViewInput>())
// Register url
registerURLPattern("app://editor/:title")
}
}
Objective-C 示例
@implementation NoteEditorViewRouter
+ (void)registerRoutableDestination {
[self registerView:[NoteEditorViewController class]];
[self registerViewProtocol:ZIKRoutable(EditorViewInput)];
// Register url
[self registerURLPattern:@"app://editor/:title"];
}
@end
然后你可以通过它的 URL 来获取路由器
ZIKAnyViewRouter.performURL("app://editor/test_note", path: .push(from: self))
Objective-C 示例
[ZIKAnyViewRouter performURL:@"app://editor/test_note" path:ZIKViewRoutePath.pushFrom(self)];
并处理 URL 方案
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
}
}
Objective-C 示例
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
if ([ZIKAnyViewRouter performURL:urlString fromSource:self.rootViewController]) {
return YES;
} else if ([ZIKAnyServiceRouter performURL:urlString]) {
return YES;
} else {
return NO;
}
}
如果你的项目对 URL 路由器有不同要求,你可以自己编写 URL 路由器。你可以创建自定义的 ZIKRouter 作为父类,并在其中添加更多强大的功能。请参阅 ZIKRouter+URLRouter.h
。
其他功能
还有其他功能,你可以在文档中找到详细信息。
- 每个路由器的自定义过渡,例如在标签栏中切换视图控制器
- Storyboard
- 在视图过渡中的 AOP 回调
- 处理自定义事件
服务路由器
除了视图外,你还可以获取任何服务模块
/// 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 示例
/// 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 最初是为 VIPER 架构设计的。但你也也可以在 MVC 或其他任何地方使用它。
这个仓库中的示例(ZIKRouterDemo)展示了如何使用 ZIKRouter 执行每种路由类型。打开 Router.xcworkspace
来运行它。
如果你想看它在一个 VIPER 架构应用中的工作方式,请访问 ZIKViper。
文件模板
你可以使用 Xcode 文件模板快速创建路由器和协议代码
模板 ZIKRouter.xctemplate
在 模板 位置。
将 ZIKRouter.xctemplate
复制到 ~/Library/Developer/Xcode/Templates/ZIKRouter.xctemplate
,然后在 Xcode -> 文件 -> 新建 -> 文件 -> 模板
中使用它。
许可
ZIKRouter 在 MIT 许可下可用。更多信息请查看 LICENSE 文件。