ZIKRouter
面向接口的模块管理器,通过协议进行依赖注入。
视图路由器可以通过一个方法执行 UIKit/AppKit 中的所有导航类型。
服务路由器可以找到并准备与其协议相对应的模块。
这是一个用于模块间解耦合和通信,基于接口进行模块管理和依赖注入的组件化路由工具。用多种方式最大限度地发挥编译检查的功能。
通过 protocol 寻找对应的模块,并用 protocol 进行依赖注入和模块通信。
服务路由器可以管理任意自定义模块。视图路由器进一步封装了界面跳转。
中文文档
特性
- Swift 和 Objective-C 支持
- iOS, macOS 和 tvOS 支持
- 为快速创建路由器提供的文件模板
- 为 UIViewController/NSViewController, UIView/NSView 和任意类提供路由
- 依赖注入,包括动态注入和静态注入
- 声明路由协议进行编译时检查。使用未声明的协议会导致编译错误。这是最有力的特性之一
- 与其协议匹配模块
- 支持 URL 路由
- 通过其协议而不是参数字典来配置模块
- 所需协议和提供协议以进行彻底解耦合
- 解耦合模块的适配器以增加兼容接口
- 故事板支持。segue的可视视图可以自动准备
- 为UIKit / AppKit中的所有转场方法和解绑方法进行封装,以及自定义转场
- 对视图转场进行错误检查
- 视图转场使用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
记住在生产环境中使用 RELEASE 版本。
对于 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];
必需参数和特殊参数
一些参数无法通过目标协议传递
-
目标类使用自定义初始化器来创建实例,路由器需要从调用者获取必需参数
-
该模块包含多个组件,您需要将这些参数传递给这些组件。这些参数不属于目标,因此不应存在于目标的协议中
您可以使用模块配置协议和自定义配置来传递参数。
我们使用另一个可路由协议EditorViewModuleInput
作为配置协议进行路由,而不是EditorViewInput
// 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触控,并实现了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
删除视图,而不需要使用弹出/消失/从父视图控制器中移除/从视图中移除。
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 路由器默认不被包含。如果您要使用它,请将子模块 pod 'ZIKRouter/URLRouter'
添加到您的 Podfile
中,并通过调用 [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
然后您可以通过它获取路由器
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
在 Templates。
将ZIKRouter.xctemplate
复制到~/Library/Developer/Xcode/Templates/ZIKRouter.xctemplate
,然后您可以在Xcode → 文件 →新建 → 文件 → 模板
中使用它。
许可证
ZIKRouter在MIT许可证下可用。有关更多信息,请参阅LICENSE文件。