Navigator
是一个用于 SwiftUI 的 Router
库。
它支持在模块化应用程序中,按功能模块实现独立的路由。
在一个由多个功能模块组成的应用程序中,我们希望消除功能模块之间的依赖关系,以便于在每个功能模块上进行独立开发。
然而,在某些情况下,目标屏幕可能来自不同的功能模块。
在这种情况下,我们希望在无需知道目标屏幕具体类型的情况下管理屏幕跳转。
Navigator
通过 Swift Package Manager 分发。
请将以下内容添加到 Package.swift
中的 dependencies 中。
.package(url: "https://github.com/taji-taji/Navigator", from: "0.0.1"),
首先,将屏幕跳转的初始屏幕包裹在 RootView 中。
import SwiftUI
import AppFeature
// ⭐️ 1️⃣ - Import `Navigator` module
import Navigator
@main
struct NavigatorExampleApp: App {
var body: some Scene {
WindowGroup {
// ⭐️ 2️⃣ - Wrap ContentView with `RootView`
RootView(viewProvider: viewProvider) { // ⭐️ 3️⃣
ContentView()
}
}
}
}
⭐️ 3️⃣ viewProvider
参数是一个闭包,其签名为 @ViewBuilder viewProvider: @escaping (any NavigationDestination) -> some View
。在本例中,一个遵循此签名的名为 viewProvider
的方法定义如下
import SwiftUI
// ⭐️ 1️⃣ - Import `Navigator` module
import Navigator
// ⭐️ 2️⃣ - Define a method to use as an argument for `RootView`
@ViewBuilder
public func viewProvider(destination: any NavigationDestination) -> some View {
// ⭐️ 3️⃣
EmptyView()
}
此方法(或闭包)将抽象的目标页面映射到其对应的具体 View
类型,因此它应该在依赖于每个功能模块的模块中定义,例如应用程序目标模块。
⭐️ 3️⃣ 在此阶段,由于尚未在每个功能模块中定义目标页面,您可以返回 EmptyView() 作为占位符。(或者,您也可以使用 fatalError()。)
当然,您也可以直接将闭包作为参数写入 RootView
。
在每个功能模块中使用 Navigator
。
在以下示例中,我们将在名为 Feature1
的模块中使用带有名为 View1
的屏幕的 Navigator
。
import SwiftUI
// ⭐️ 1️⃣ - Import `Navigator` module
import Navigator
public struct View1: View, Navigatable /* ⭐️ 2️⃣ - Conform to the `Navigatable` protocol */ {
// ⭐️ 3️⃣ - Implement a `Destination` type that conforms to the `NavigationDestination` protocol.
// The `Destination` type abstractly represents the screens to which transitions occur from this screen.
// In this example, `view3` is a screen located in the `Feature2` module, not in the `Feature1` module.
// However, you do not need to know the specific type of `View` here.
// In other words, there is no need to depend on `Feature2`.
public enum Destination: NavigationDestination {
case view2
case view3(fromView: String)
}
// ⭐️ 4️⃣ - Implement a `navigator` property that conforms to the `Navigatable` protocol.
@EnvironmentObject public var navigator: Navigator
public init() {}
public var body: some View {
VStack {
Text("View1")
Button("to View2") {
// ⭐️ 5️⃣ - By conforming to the `Navigatable` protocol, you can use the `navigate(to:)` protocol method.
// Call the `navigate(to:)` method wherever you want to perform a screen transition.
// Here, since we want to transition to `view2`, we pass `Destination.view2` as the argument.
navigate(to: Destination.view2)
}
Button("to View3") {
// ✅ Since `navigate(to:)` is a wrapper around `navigator.navigate(to:)`, you can also directly use `navigator.navigate(to:)`.
navigator.navigate(to: Destination.view3(fromView: "View1"))
}
}
.navigationTitle("View1")
// ⭐️ 6️⃣ - Use the `navigatable(for:)` modifier on the `View` in the body.
// ⚠️ Without this, screen transitions will not be possible, so make sure not to forget it.
.navigatable(for: Destination.self)
}
}
或者使用 @Navigatable
宏。 通过应用 @Navigatable
宏,可以省略以下内容
Navigatable
协议navigator
属性import SwiftUI
import Navigator
// ⭐️ 1️⃣ - Attach `@Navigatable` macro to View
@Navigatable
public struct View1: View /* ⭐️ 2️⃣ - No need to write `Navigatable` protocol */ {
public enum Destination: NavigationDestination {
case view2
case view3(fromView: String)
}
// ⭐️ 3️⃣ - No need to write `navigator` property
public init() {}
public var body: some View {
VStack {
Text("View1")
Button("to View2") {
navigate(to: Destination.view2)
}
Button("to View3") {
navigate(to: Destination.view3(fromView: "View1"))
}
}
.navigationTitle("View1")
.navigatable(for: Destination.self)
}
}
警告
遵循 Navigatable
协议的 View
必须在视图层级的顶层有一个 RootView
。 这是因为它内部使用了由 RootView
注入的 EnvironmentObject
。
由于上述原因,当使用 Preview 时,您需要将其包裹在 RootView
中。 这是 View1
的预览示例
#Preview {
RootView(
viewProvider: { _ in
Text("Preview")
},
content: {
View1()
}
)
}
完成在步骤 1 中定义的 viewProperty
方法。
import SwiftUI
import Navigator
import Feature1
import Feature2
@ViewBuilder
public func viewProvider(destination: any NavigationDestination) -> some View {
switch destination {
// ⭐️ 1️⃣ - Return the `View` corresponding to the `View1.Destination` enum.
case let d as View1.Destination:
switch d {
case .view2:
View2()
case let .view3(fromView):
// ⭐️ 2️⃣ - `View3` is a type from the `Feature2` module.
View3(fromView: fromView)
}
default:
fatalError()
}
}
import SwiftUI
import Navigator
@Navigatable
public struct ContentView: View {
public enum Destination: NavigationDestination {
case view1
case view3(fromView: String)
}
public init() {}
public var body: some View {
List {
// ⭐️ 1️⃣ - Using `Navigator` with `NavigationLink` is straightforward:
// simply pass a `Destination` value to the `value` parameter,
// and screen transitions will occur when the `NavigationLink` is tapped.
NavigationLink("to View1", value: Destination.view1)
NavigationLink("to View3", value: Destination.view3(fromView: "ContentView"))
}
.navigationTitle("ContentView")
.navigatable(for: Destination.self)
}
}
import SwiftUI
import Navigator
@Navigatable
public struct MyView: View {
public struct Destination: NavigationDestination {}
public init() {}
public var body: some View {
VStack {
Button("Go back previous page") {
// ⭐️ 1️⃣ - Go back previous page
navigateBack()
}
Button("Go back two pages") {
// ⭐️ 2️⃣ - Specify the number of pages to go back
navigateBack(2)
}
Button("Go back to root page") {
// ⭐️ 3️⃣ - Go back to root page
navigateBackToRoot()
}
}
.navigationTitle("MyView")
.navigatable(for: Destination.self)
}
}
import SwiftUI
import Navigator
@Navigatable
public struct View3: View {
public enum Destination: NavigationDestination {
case view4
}
@State private var isPresented = false
private let fromView: String
public init(fromView: String) {
self.fromView = fromView
}
public var body: some View {
VStack {
Text("View3 (from: \(fromView))")
Button("to View4") {
isPresented = true
}
}
.sheet(isPresented: $isPresented) {
// ⭐️ 1️⃣ - When displaying a sheet, the displayed screen becomes the root of the navigation.
// Therefore, use the `rootView(with:)` method to transition to a screen wrapped in `RootView`.
rootView(with: Destination.view4)
}
.navigationTitle("View3")
}
}