一个导航库,它将 SwiftUI 中的导航逻辑与视图分离,并允许您在应用程序中以编程方式动态导航到其他视图。
navigate(to:)
pop()
popToRoot()
struct DetailScreen: ScreenView {
@EnvironmentObject var navigator: Navigator<ScreenID, AppViewFactory>
@State var showNextScreen: Bool = false
var screenId: ScreenID
var body: some View {
Button("Next") {
navigator.navigate(to: anotherScreen())
}
.navigationTitle("Detail Screen")
.bindNavigation(self, binding: $showNextScreen)
}
}
创建一个枚举,将应用程序中的屏幕映射到相应的屏幕类型,并包含任何所需的值。确保它符合 Hashable
协议
enum ScreenID: Hashable {
case rootScreen
case detailScreen(detailID: String)
}
然后创建一个符合 ViewFactory
协议的类,并实现 makeView(screenType:)
方法,以便给定的 ScreenID
枚举返回关联的 View
。
class AppViewFactory: ViewFactory {
@ViewBuilder
func makeView(screenType: ScreenWrapper<ScreenID>) -> some View {
switch screenType {
case .screenWrapper(let screenId):
switch screenId {
case .rootScreen:
RootScreen()
case .detailScreen:
DetailScreen(screenId: screenId!)
case .none:
// EmptyView is fine in the exhaustive case
// as this is just a placeholder until the
// Navigation state is instantiated. The
// app will never need to navigate to this
EmptyView()
}
}
}
}
在 @main
应用程序声明中,将您的根视图作为导航堆栈的顶层视图(或者您想放置带有编程导航的 NavigationView
的任何位置),使用 with(_:)
方法初始化 NavigationView
,以注入您的 Navigator
实例。
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
NavigationView.with(Navigator(rootScreen: ScreenID.rootScreen, viewFactory: AppViewFactory()) {
RootScreen()
}
}
}
}
对于每个顶层视图,使其符合 ScreenView
协议,并应用 bindNavigation(_:binding:)
视图修饰符
struct RootScreen: ScreenView {
@EnvironmentObject var navigator: Navigator<ScreenID, AppViewFactory>
@State var showNextScreen: Bool = false
var screenId = .rootScreen
var body: some View {
List {
Button("Navigate to Deatil Screen") {
// TODO
}
}
.navigationTitle("Root Screen")
.bindNavigation(self, binding: $showNextScreen)
}
}
struct DetailScreen: ScreenView {
@EnvironmentObject var navigator: Navigator<ScreenID, AppViewFactory>
@State var showNextScreen: Bool = false
var screenId: ScreenID
var body: some View {
VStack {
Text("Here is some amazing detail.")
Button("Go Back") {
// TODO
}
.tint(.red)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.automatic)
.controlSize(.large)
}
.navigationTitle("Detail Screen")
.bindNavigation(self, binding: $showNextScreen)
}
}
请注意,除了 RootScreen
之外,所有其他屏幕都应该在其 ScreenID
枚举值中具有一个 id
,以便区分导航堆栈中的相同屏幕。 这可以是业务逻辑 ID detailID
的形式,也可以是一些没有业务逻辑但仅用于识别屏幕的默认 ID
enum ScreenID: Hashable {
// ...
case anotherScreen(id: UUID = UUID())
}
完成所有设置后,我们可以在 ScreenView
视图中使用 Navigator
实例来执行系统级别的视图导航
struct RootScreen: ScreenView {
@EnvironmentObject var navigator: Navigator<ScreenID, AppViewFactory>
@State var showNextScreen: Bool = false
var screenId = .rootScreen
var body: some View {
List {
Button("Navigate to Deatil Screen") {
// Use navigate(to:) to navigate to another ScreenView
// This can be anywhere at the view level
// This can also be called at an abstraction layer like a ViewModel
navigator.navigate(to: ScreenID.detailScreen(detailID: "detail-123"))
}
}
.navigationTitle("Root Screen")
.bindNavigation(self, binding: $showNextScreen)
}
}
struct DetailScreen: ScreenView {
@EnvironmentObject var navigator: Navigator<ScreenID, AppViewFactory>
@State var showNextScreen: Bool = false
var screenId: ScreenID
var body: some View {
VStack {
Text("Here is some amazing detail.")
Button("Go Back") {
// Use pop() to navigate back to the
// previous view by popping the current
navigator.pop()
}
.tint(.red)
.buttonStyle(.borderedProminent)
.buttonBorderShape(.automatic)
.controlSize(.large)
}
.navigationTitle("Detail Screen")
.bindNavigation(self, binding: $showNextScreen)
}
}
如前所述,Navigator
类具有属性 navStack
,它保留 ScreenView
关联的 ScreenID
枚举值的堆栈 (OrderedDictionary),这些值当前在 NavigationView 中处于活动状态。 客户端可以使用它来执行自定义导航逻辑。
/// Holds the ordered uniqe set of screens that makes up the Navigation State of the client app,
/// along with its associated Boolean subject, which toggles the NavigationLink.isActive
var navStack: OrderedDictionary<ScreenIdentifer, CurrentValueSubject<Bool, Never>> { get }
例如,我们可以使用它在 Navigator
上创建一个扩展函数,名为 popToDetailWithSpecificIdOrRoot(id: String)
extension Navigator where ScreenIdentifer == ScreenID {
func popToDetailWithSpecificIdOrRoot(id: String) {
// Since all screens that have been pushed onto the NavigationView
// are stored in the navStack as keys, we can simply search through
// them for the specific detail screen with the called id
if let detailScreen: ScreenID = navStack.keys.elements.first(where: {
if case ScreenID.detailScreen(let detailID) = $0 {
return detailID == id
}
return false
}) {
// Using the first(where:) collections function
// if we find the detail screen with the called id
// then the navStack Dictionary can be keyed
// on the found detailScreen to return the Combine subject
// that controls the NavigationLink.isActive binding
// for that detailScreen
let detailScreenIsActiveBinding = navStack[detailScreen]!
// Sending the false flag to the NavigationLink.isActive binding
// will dismiss all views off the NavigationView stack to reveal
// the detailScreen
detailScreenIsActiveBinding.send(false)
} else {
// if the detail screen is not found in the current stack
// then we default to pop all views off the stack to reveal the
// rootScreen view
let rootScreenNavigationIsActiveBinding = navStack.elements[0]
rootScreenNavigationIsActiveBinding.value.send(false)
// could also call self.popToRoot()
}
}
}
由于 Navigator
类可以是一个单例,而不与任何特定视图耦合,因此我们可以在应用程序中的任何位置调用 popToDetailWithSpecificIdOrRoot(id:)
。
@main
struct MyApp: App {
// Keep reference to the navigator, either as a local property or in an abstraction
// layer, such as an AppCoordinator
let navigator = Navigator(rootScreen: ScreenID.rootScreen, viewFactory: AppViewFactory())
var body: some Scene {
WindowGroup {
NavigationView.with(navigator) {
RootScreen()
}
.task {
// mimic a system event, such as a notification
// create an artificial delay of 10 seconds after the app starts up
try! await Task.sleep(nanoseconds: 10_000_000_000)
// User navigates to different views, adding to the navStack
// However far along the user is, this will pop to the first
// detailScreen with called id, or pop to the root screen
navigator.popToDetailWithSpecificIdOrRoot(id: "detail-123")
}
}
}
}
非常感谢 Obsured Pixels' 的精彩文章,它启发了这个库。