适用于iOS的应用程序URL路由器(仅限Swift)。受到URLNavigator的启发。
利用 @used 和 @section(Swift 6.3 起正式支持)可以将数据写入 Mach-O section, 再结合 Swift Macro, 就可以实现 OC 时代各种解耦和的, 用于注册信息的能力了. 本框架也支持了以这种方式注册路由
注册 UIViewController
extension Route.Key {
// 注意 chat 变量名要和赋值的字符串完全一致
static let chat: Route.Key = "chat"
}
@Routable(.chat)
class ChatViewController: UIViewController {
static func make(with param: Route.Param) -> ChatViewController? {
return .init()
}
// ... other methods ...
}
@Routable("setting")
class SettingViewController: UIViewController {
static func make(with param: Route.Param) -> SettingViewController? {
return .init()
}
// ... other methods ...
}
注册 action:
extension Route.Key {
// 注意 testKey 变量名要和赋值的字符串完全一致
static let testKey: Self = "testKey"
}
struct Foo {
#route(key: .testKey, action: { params in
print("testKey triggered nested")
})
}
用 Xcode 打开 ReerRouterDemo(Swift Package Manager)。
Xcode 26.4 + (Swift 6.3+)
对于旧版 Xcode (16.0-26.3),请使用 2.2.8 版本,该版本使用实验性的
@_section和@_used属性。
iOS 13 +
swift-syntax 601.0.1+
已停止维护 CocoaPods 集成。2.3.2 是最后一个 CocoaPods 版本。 新版本请迁移到 Swift Package Manager。
若仍需使用 CocoaPods,请固定 2.3.2:
pod 'ReerRouter', '2.3.2'由于 CocoaPods 不支持直接使用 Swift Macro,ReerRouter 2.3.2 会从与 pod 版本对应的 GitHub Release 下载预编译的 universal 宏插件(下载失败时回退到源码构建)。依赖方仍需设置 s.pod_target_xcconfig 来加载该插件:
s.pod_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_BUILD_DIR}/ReerRouter/MacroPlugin/ReerRouterMacros#ReerRouterMacros'
}
s.user_target_xcconfig = {
'OTHER_SWIFT_FLAGS' => '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_BUILD_DIR}/ReerRouter/MacroPlugin/ReerRouterMacros#ReerRouterMacros'
}
或者, 如果不使用s.pod_target_xcconfig, 也可以在 podfile 中添加如下脚本统一处理:
post_install do |installer|
installer.pods_project.targets.each do |target|
reerrouter_dependency = target.dependencies.find { |d| ['ReerRouter'].include?(d.name) }
if reerrouter_dependency
puts "Adding Rhea Swift flags to target: #{target.name}"
target.build_configurations.each do |config|
swift_flags = config.build_settings['OTHER_SWIFT_FLAGS'] ||= ['$(inherited)']
plugin_flag = '-Xfrontend -load-plugin-executable -Xfrontend ${PODS_BUILD_DIR}/ReerRouter/MacroPlugin/ReerRouterMacros#ReerRouterMacros'
unless swift_flags.join(' ').include?(plugin_flag)
swift_flags.concat(plugin_flag.split)
end
end
config.build_settings['OTHER_SWIFT_FLAGS'] = swift_flags
end
end
end
end
rsync 权限错误,需关闭用户脚本沙盒:
在工程的 Build Settings 中搜索 User Script Sandboxing,将 ENABLE_USER_SCRIPT_SANDBOXING 设为 No。这可解决 Xcode 严格沙盒限制导致的 CocoaPods 脚本执行失败问题。
// Package.swift
let package = Package(
name: "APackageDependOnReerRouter",
platforms: [.iOS(.v13)],
products: [
.library(name: "APackageDependOnReerRouter", targets: ["APackageDependOnReerRouter"]),
],
dependencies: [
.package(url: "https://github.com/reers/ReerRouter.git", from: "2.4.0")
],
targets: [
.target(
name: "APackageDependOnReerRouter",
dependencies: [
.product(name: "ReerRouter", package: "ReerRouter")
]
),
]
)
Route.Key有两种 Mode.
/// myapp://example.com/over/there?name=phoenix#nose
/// \______/\_________/\_________/ \__________/ \__/
/// | | | | |
/// scheme host path queries fragment
/// \_________/
/// |
/// route key
/// myapp://example.com/over/there?name=phoenix#nose
/// \______/\_________/\_________/ \__________/ \__/
/// | | | | |
/// scheme host path queries fragment
/// |
/// |
/// route key
可以通过实现 RouterConfigable 协议来配置为 Mode 2
extension Router: RouterConfigable {
public static var host: String {
return "example.com"
}
}
- 注册一个 action
Router.shared.registerAction(with: "abc_action") { _ in
print("action executed.")
}
- 通过 UIViewController 类型和 Route.Key 常量注册一个路由
extension Route.Key {
static let userPage: Self = "user"
}
Router.shared.register(UserViewController.self, forKey: .userPage)
Router.shared.register(UserViewController.self, forKey: "user")
- 通过 UIViewController 类型和字符串 key 注册一个路由
Router.shared.registerPageClasses(with: ["preference": PreferenceViewController.self])
- 通过 UIViewController 字符串和字符串 key 注册一个路由
Router.shared.registerPageClasses(with: ["preference": "ReerRouter_Example.PreferenceViewController"])
- 使用 Swift Macro 来注册
extension Route.Key {
static let testKey: Self = "testKey"
}
struct Foo {
#route(key: .testKey, action: { params in
print("testKey triggered nested")
})
}
extension Route.Key {
static let chat: Route.Key = "chat"
}
@Routable(.chat)
class ChatViewController: UIViewController {
static func make(with param: Route.Param) -> ChatViewController? {
return .init()
}
// ... other methods ...
}
@Routable("setting")
class SettingViewController: UIViewController {
static func make(with param: Route.Param) -> SettingViewController? {
return .init()
}
// ... other methods ...
}
Router.shared.host = "phoenix.com"
现在 Route.Key 仅表示 path, 然后其他所有注册方法与 Mode1 相同. ("path", "/path" 两种表达方式都支持)
class UserViewController: UIViewController, Routable {
var params: [String: Any]
init(params: [String: Any]) {
self.params = params
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
static func make(with param: Route.Param) -> UserViewController? {
return .init(params: param.allParams)
}
}
Router.shared.executeAction(byKey: "abc_action")
// Mode 1
Router.shared.open("myapp://abc_action")
// Mode 2
Router.shared.open("myapp://phoenix.com/abc_action")
Router.shared.present(byKey: .userPage, embedIn: UINavigationController.self, userInfo: [
"name": "apple",
"id": "123123"
])
// Mode 1
Router.shared.open("myapp://user?name=phoenix")
Router.shared.push("myapp://user?name=phoenix")
Router.shared.present("myapp://user?name=phoenix")
// Mode 2
Router.shared.open("myapp://phoenix.com/user?name=phoenix")
Router.shared.push("myapp://phoenix.com/user?name=phoenix")
Router.shared.present("myapp://phoenix.com/user?name=phoenix")
extension RouteManager: RouterDelegate {
func router(_ router: Router, willOpenURL url: URL, userInfo: [String : Any]) -> URL? {
print("will open \(url)")
if let _ = url.absoluteString.range(of: "google") {
return URL(string: url.absoluteString + "&extra1=234244&extra2=afsfafasd")
} else if let _ = url.absoluteString.range(of: "bytedance"), !isUserLoggedIn() {
print("intercepted by delegate")
return nil
}
return url
}
func router(_ router: Router, didOpenURL url: URL, userInfo: [String : Any]) {
print("did open \(url) success")
}
func router(_ router: Router, didFailToOpenURL url: URL, userInfo: [String : Any]) {
print("did fail to open \(url)")
}
func router(_ router: Router, didFallbackToURL url: URL, userInfo: [String: Any]) {
print("did fallback to \(url)")
}
}
- 使用
route_fallback_urlkey 作为出错时的备用页面.
Router.shared.open("myapp://unregisteredKey?route_fallback_url=myapp%3A%2F%2Fuser%3Fname%3Di_am_fallback" )
- 实现
redirectURLWithRouteParam(_:)方法,为视图控制器重定向到一个新的URL。
class PreferenceViewController: UIViewController, Routable {
static func make(with param: Route.Param) -> PreferenceViewController? {
return .init()
}
static func redirectURLWithRouteParam(_ param: Route.Param) -> URL? {
if let value = param.allParams["some_key"] as? String, value == "redirect" {
return URL(string: "myapp://new_preference")
}
return nil
}
}
public let AppRouter = Router.shared
AppRouter.open("myapp://user")
NotificationCenter.default.addObserver(
forName: Notification.Name.routeWillOpenURL,
object: nil,
queue: .main
) { notification in
if let param = notification.userInfo?[Route.notificationUserInfoKey] as? Route.Param {
print("notification: route will open \(param.sourceURL)")
}
}
NotificationCenter.default.addObserver(
forName: Notification.Name.routeDidOpenURL,
object: nil,
queue: .main
) { notification in
if let param = notification.userInfo?[Route.notificationUserInfoKey] as? Route.Param {
print("notification: route did open \(param.sourceURL)")
}
}
public typealias UserTransition = (
_ fromNavigationController: UINavigationController?,
_ fromViewController: UIViewController?,
_ toViewController: UIViewController
) -> Bool
public enum TransitionExecutor {
/// Transition will be handled by router automatically.
case router
/// Transition will be handled by user who invoke the router `push` or `present` method.
case user(UserTransition)
/// Transition will be handled by user who invoke the router `push` or `present` method.
case delegate
}
let transition: Route.UserTransition = { fromNavigationController, fromViewController, toViewController in
toViewController.transitioningDelegate = self.animator
toViewController.modalPresentationStyle = .currentContext
// Use the router found view controller directly, or just handle transition by yourself.
// fromViewController?.present(toViewController, animated: true)
self.present(toViewController, animated: true)
return true
}
AppRouter.present(user.urlString, transitionExecutor: .user(transition))
路由器处理 UIViewController 打开方式的时候, 按照以下优先级顺序选取 style:
`Router` 的属性 `preferredOpenStyle` <
控制器实现的 `Routable` 协议属性 `preferredOpenStyle` <
使用者调用的方法, 比如, 如果你调用的是 `Router.push(...)`, 那控制器就以 push 的方式打开.
- 用
route_no_animation字段来禁掉转场动画
Router.shared.open("myapp://user?name=google&route_no_animation=1")
在一些特殊场景对路由进行拦截, 返回 false 表示对该 url 进行拦截.
Router.shared.addInterceptor(forKey: .userPage) { (_) -> Bool in
print("intercepted user page")
return true
}
Router.shared.addInterceptor(forKey: .userPage) { (params) -> Bool in
print("intercepted user page")
if let name = params.allParams["name"] as? String, name == "google" {
print("intercepted user page success")
return false
}
return true
}
extension Router: RouterConfigable {
// 由该配置关闭自动获取
public static var registrationMode: RegistrationMode { return .manual }
}
// 然后在合适的时机调用
Router.shared.registerRoutes()
phoenix, x.rhythm@qq.com
ReerRouter is available under the MIT license. See the LICENSE file for more info.