如何使用 SwiftUI 弹出到 Root 视图?

最后,使用 Beta 5,我们可以通过编程方式弹出到父视图。然而,在我的应用程序中,有几个地方的视图有一个“保存”按钮,结束了几个步骤的过程,并返回到开始。在 UIKit 中,我使用 popToRootViewController () ,但是我无法在 SwiftUI 中找到同样的方法。

下面是我试图实现的模式的一个简单示例。

我该怎么做?

import SwiftUI


struct DetailViewB: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Text("This is Detail View B.")


Button(action: { self.presentationMode.value.dismiss() } )
{ Text("Pop to Detail View A.") }


Button(action: { /* How to do equivalent to popToRootViewController() here?? */ } )
{ Text("Pop two levels to Master View.") }


}
}
}


struct DetailViewA: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var body: some View {
VStack {
Text("This is Detail View A.")


NavigationLink(destination: DetailViewB() )
{ Text("Push to Detail View B.") }


Button(action: { self.presentationMode.value.dismiss() } )
{ Text("Pop one level to Master.") }
}
}
}


struct MasterView: View {
var body: some View {
VStack {
Text("This is Master View.")


NavigationLink(destination: DetailViewA() )
{ Text("Push to Detail View A.") }
}
}
}


struct ContentView: View {
var body: some View {
NavigationView {
MasterView()
}
}
}
67772 次浏览

据我所知,在目前的 beta 5中没有任何简单的方法可以做到这一点。我找到的唯一方法很蹩脚,但很管用。

基本上,添加一个发布者到您的 DetailViewA,它将从 DetailViewB 触发。在 DetailViewB 中,取消视图并通知发布者,发布者本身将关闭 DetailViewA。

    struct DetailViewB: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var publisher = PassthroughSubject<Void, Never>()


var body: some View {
VStack {
Text("This is Detail View B.")


Button(action: { self.presentationMode.value.dismiss() } )
{ Text("Pop to Detail View A.") }


Button(action: {
DispatchQueue.main.async {
self.presentationMode.wrappedValue.dismiss()
self.publisher.send()
}
} )
{ Text("Pop two levels to Master View.") }


}
}
}


struct DetailViewA: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
var publisher = PassthroughSubject<Void, Never>()


var body: some View {
VStack {
Text("This is Detail View A.")


NavigationLink(destination: DetailViewB(publisher:self.publisher) )
{ Text("Push to Detail View B.") }


Button(action: { self.presentationMode.value.dismiss() } )
{ Text("Pop one level to Master.") }
}
.onReceive(publisher, perform: { _ in
DispatchQueue.main.async {
print("Go Back to Master")
self.presentationMode.wrappedValue.dismiss()
}
})
}
}

而且 Beta 6还是没有解决方案。

我找到了另一种回到根目录的方法,但这次我失去了动画效果,直接回到根目录。 其思想是强制刷新根视图,这样可以清除导航堆栈。

但最终只有苹果能够提供一个合适的解决方案,因为 SwiftUI 无法管理导航栈。

注意: 下面通知的简单解决方案适用于 iOS,而不适用于 手表操作系统,因为 watch OS 在两个导航级别之后将根视图从内存中清除。但是让一个外部类来管理 watch OS 的状态应该可以。

struct DetailViewB: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


@State var fullDissmiss:Bool = false
var body: some View {
SGNavigationChildsView(fullDissmiss: self.fullDissmiss){
VStack {
Text("This is Detail View B.")


Button(action: { self.presentationMode.wrappedValue.dismiss() } )
{ Text("Pop to Detail View A.") }


Button(action: {
self.fullDissmiss = true
} )
{ Text("Pop two levels to Master View with SGGoToRoot.") }
}
}
}
}


struct DetailViewA: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


@State var fullDissmiss:Bool = false
var body: some View {
SGNavigationChildsView(fullDissmiss: self.fullDissmiss){
VStack {
Text("This is Detail View A.")


NavigationLink(destination: DetailViewB() )
{ Text("Push to Detail View B.") }


Button(action: { self.presentationMode.wrappedValue.dismiss() } )
{ Text("Pop one level to Master.") }


Button(action: { self.fullDissmiss = true } )
{ Text("Pop one level to Master with SGGoToRoot.") }
}
}
}
}


struct MasterView: View {
var body: some View {
VStack {
Text("This is Master View.")
NavigationLink(destination: DetailViewA() )
{ Text("Push to Detail View A.") }
}
}
}


struct ContentView: View {


var body: some View {
SGRootNavigationView{
MasterView()
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif


struct SGRootNavigationView<Content>: View where Content: View {
let cancellable = NotificationCenter.default.publisher(for: Notification.Name("SGGoToRoot"), object: nil)


let content: () -> Content


init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}


@State var goToRoot:Bool = false


var body: some View {
return
Group{
if goToRoot == false{
NavigationView {
content()
}
}else{
NavigationView {
content()
}
}
}.onReceive(cancellable, perform: {_ in
DispatchQueue.main.async {
self.goToRoot.toggle()
}
})
}
}


struct SGNavigationChildsView<Content>: View where Content: View {
let notification = Notification(name: Notification.Name("SGGoToRoot"))


var fullDissmiss:Bool{
get{ return false }
set{ if newValue {self.goToRoot()} }
}


let content: () -> Content


init(fullDissmiss:Bool, @ViewBuilder content: @escaping () -> Content) {
self.content = content
self.fullDissmiss = fullDissmiss
}


var body: some View {
return Group{
content()
}
}


func goToRoot(){
NotificationCenter.default.post(self.notification)
}
}

我没有 没错相同的问题,但我有代码,改变的根视图从一个不支持导航堆栈,一个做。诀窍在于我没有在 SwiftUI 中这样做——我在 SceneDelegate中这样做,然后用一个新的 UIHostingController代替它。

下面是我的 SceneDelegate的一个简化摘录:

    func changeRootToOnBoarding() {
guard let window = window else {
return
}


let onBoarding = OnBoarding(coordinator: notificationCoordinator)
.environmentObject(self)


window.rootViewController = UIHostingController(rootView: onBoarding)
}


func changeRootToTimerList() {
guard let window = window else {
return
}


let listView = TimerList()
.environmentObject(self)
window.rootViewController = UIHostingController(rootView: listView)
}

因为 SceneDelegate把自己放在环境中,所以任何子视图都可以添加

    /// Our "parent" SceneDelegate that can change the root view.
@EnvironmentObject private var sceneDelegate: SceneDelegate

然后对委托调用公共函数。我认为,如果你做了类似的事情,保持了 View,但创造了一个新的 UIHostingController,并取代 window.rootViewController它可能为您工作。

我想出了另一个有效的方法,但是感觉还是很奇怪。它也仍然动画两个屏幕消失,但它是一个 一点点清洁。您可以 A)向下传递一个闭包到后续的详细信息屏幕,或者 B)向 Details B 传递 Details A 的 presentationMode。这两种方法都需要在试图取消 detailA 之前先取消 detailB,然后再延迟一小段时间,使 Details A 重新出现在屏幕上。

let minDelay = TimeInterval(0.001)


struct ContentView: View {
var body: some View {
NavigationView {
VStack {
NavigationLink("Push Detail A", destination: DetailViewA())
}.navigationBarTitle("Root View")
}
}
}


struct DetailViewA: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


var body: some View {
VStack {
Spacer()


NavigationLink("Push Detail With Closure",
destination: DetailViewWithClosure(dismissParent: { self.dismiss() }))


Spacer()


NavigationLink("Push Detail with Parent Binding",
destination: DetailViewWithParentBinding(parentPresentationMode: self.presentationMode))


Spacer()


}.navigationBarTitle("Detail A")
}


func dismiss() {
print ("Detail View A dismissing self.")
presentationMode.wrappedValue.dismiss()
}
}


struct DetailViewWithClosure: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


@State var dismissParent: () -> Void


var body: some View {
VStack {
Button("Pop Both Details") { self.popParent() }
}.navigationBarTitle("Detail With Closure")
}


func popParent() {
presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + minDelay) { self.dismissParent() }
}
}


struct DetailViewWithParentBinding: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


@Binding var parentPresentationMode: PresentationMode


var body: some View {
VStack {
Button("Pop Both Details") { self.popParent() }
}.navigationBarTitle("Detail With Binding")
}


func popParent() {
presentationMode.wrappedValue.dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + minDelay) { self.parentPresentationMode.dismiss() }
}
}

我对 SwiftUI 的工作原理和结构思考得越多,我就越不认为 Apple 威尔提供了与 popToRootViewController相当的东西,或者对导航栈进行了其他直接编辑。它与 SwiftUI 构建视图结构的方式背道而驰,因为它允许子视图深入到父视图的状态并对其进行操作。这就是 没错,这些方法所做的,但是它们显式地、公开地这样做。如果不提供对其自身状态的访问,DetailViewA就不能创建任何一个目标视图,这意味着作者必须考虑提供这种访问的含义。

我弄明白了如何在 SwiftUI 中使用复杂的导航。诀窍是收集视图的所有状态,这些状态告诉您是否显示了视图。

首先定义一个 NavigationController。我已经添加了 tabview 选项卡的选项和布尔值,说明是否显示了特定的视图:

import SwiftUI


final class NavigationController: ObservableObject  {


@Published var selection: Int = 1


@Published var tab1Detail1IsShown = false
@Published var tab1Detail2IsShown = false


@Published var tab2Detail1IsShown = false
@Published var tab2Detail2IsShown = false
}

设置带有两个选项卡的 tabview,并将 NavigationController.select 绑定到 tabview:

import SwiftUI


struct ContentView: View {


@EnvironmentObject var nav: NavigationController


var body: some View {


TabView(selection: self.$nav.selection) {


FirstMasterView()
.tabItem {
Text("First")
}
.tag(0)


SecondMasterView()
.tabItem {
Text("Second")
}
.tag(1)
}
}


}

例如,这是一个导航堆栈

import SwiftUI


struct FirstMasterView: View {


@EnvironmentObject var nav: NavigationController


var body: some View {
NavigationView {
VStack {


NavigationLink(destination: FirstDetailView(), isActive: self.$nav.tab1Detail1IsShown) {
Text("go to first detail")
}
} .navigationBarTitle(Text("First MasterView"))
}
}
}


struct FirstDetailView: View {


@EnvironmentObject var nav: NavigationController
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


var body: some View {


VStack(spacing: 20) {
Text("first detail View").font(.title)


NavigationLink(destination: FirstTabLastView(), isActive: self.$nav.tab1Detail2IsShown) {
Text("go to last detail on nav stack")
}


Button(action: {
self.nav.tab2Detail1IsShown = false // true will go directly to detail
self.nav.tab2Detail2IsShown = false


self.nav.selection = 1
}) {
Text("Go to second tab")
}
}


// In case of collapsing all the way back
// there is a bug with the environment object
// to go all the way back I have to use the presentationMode
.onReceive(self.nav.$tab1Detail2IsShown, perform: { (out) in
if out ==  false {
self.presentationMode.wrappedValue.dismiss()
}
})
}
}


struct FirstTabLastView: View {
@EnvironmentObject var nav: NavigationController


var body: some View {
Button(action: {
self.nav.tab1Detail1IsShown = false
self.nav.tab1Detail2IsShown = false
}) {
Text("Done and go back to beginning of navigation stack")
}
}
}

这种方法是面向 SwiftUI 状态的。

下面是我的缓慢的、动画的、有点粗糙的、使用 onAppear 的弹出解决方案,适用于 Xcode 11和 iOS 13.1:

import SwiftUI
import Combine




struct NestedViewLevel3: View {
@Binding var resetView:Bool
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


var body: some View {
VStack {
Spacer()
Text("Level 3")
Spacer()
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Back")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)}
Spacer()
Button(action: {
self.$resetView.wrappedValue = true
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Reset")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)}
Spacer()
}
.navigationBarBackButtonHidden(false)
.navigationBarTitle("Level 3", displayMode: .inline)
.onAppear(perform: {print("onAppear level 3")})
.onDisappear(perform: {print("onDisappear level 3")})
}
}


struct NestedViewLevel2: View {
@Binding var resetView:Bool
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


var body: some View {
VStack {
Spacer()
NavigationLink(destination: NestedViewLevel3(resetView:$resetView)) {
Text("To level 3")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.gray)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)
.shadow(radius: 10)
}
Spacer()
Text("Level 2")
Spacer()
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Back")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)}
Spacer()
}
.navigationBarBackButtonHidden(false)
.navigationBarTitle("Level 2", displayMode: .inline)
.onAppear(perform: {
print("onAppear level 2")
if self.$resetView.wrappedValue {
self.presentationMode.wrappedValue.dismiss()
}
})
.onDisappear(perform: {print("onDisappear level 2")})
}
}


struct NestedViewLevel1: View {
@Binding var resetView:Bool
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>


var body: some View {
VStack {
Spacer()
NavigationLink(destination: NestedViewLevel2(resetView:$resetView)) {
Text("To level 2")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.gray)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)
.shadow(radius: 10)
}
Spacer()
Text("Level 1")
Spacer()
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Text("Back")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.blue)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)}
Spacer()
}
.navigationBarBackButtonHidden(false)
.navigationBarTitle("Level 1", displayMode: .inline)
.onAppear(perform: {
print("onAppear level 1")
if self.$resetView.wrappedValue {
self.presentationMode.wrappedValue.dismiss()
}
})
.onDisappear(perform: {print("onDisappear level 1")})
}
}


struct RootViewLevel0: View {
@Binding var resetView:Bool
var body: some View {
NavigationView {
VStack {
Spacer()
NavigationLink(destination: NestedViewLevel1(resetView:$resetView)) {
Text("To level 1")
.padding(.horizontal, 15)
.padding(.vertical, 2)
.foregroundColor(Color.white)
.clipped(antialiased: true)
.background(
RoundedRectangle(cornerRadius: 20)
.foregroundColor(Color.gray)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 40, alignment: .center)
)
.shadow(radius: 10)
}
//.disabled(false)
//.hidden()
Spacer()
}
}
//.frame(width:UIScreen.main.bounds.width,height:  UIScreen.main.bounds.height - 110)
.navigationBarTitle("Root level 0", displayMode: .inline)
.navigationBarBackButtonHidden(false)
.navigationViewStyle(StackNavigationViewStyle())
.onAppear(perform: {
print("onAppear root level 0")
self.resetNavView()
})
.onDisappear(perform: {print("onDisappear root level 0")})
}


func resetNavView(){
print("resetting objects")
self.$resetView.wrappedValue = false
}


}




struct ContentView: View {
@State var resetView = false
var body: some View {
RootViewLevel0(resetView:$resetView)
}
}


struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

NavigationLink上将视图修饰符 isDetailLink设置为 false是使 pop-to-root 工作的关键。isDetailLink在默认情况下是 true,并且适用于包含的视图。例如,在 iPad 上,分割视图是分开的,而 isDetailLink确保目标视图将显示在右侧。因此,将 isDetailLink设置为 false意味着目标视图将始终被推送到导航堆栈上; 因此总是可以弹出。

NavigationLink上将 isDetailLink设置为 false的同时,将 isActive绑定传递给每个后续的目标视图。最后,当你想弹出到根视图,设置值为 false,它会自动弹出一切:

import SwiftUI


struct ContentView: View {
@State var isActive : Bool = false


var body: some View {
NavigationView {
NavigationLink(
destination: ContentView2(rootIsActive: self.$isActive),
isActive: self.$isActive
) {
Text("Hello, World!")
}
.isDetailLink(false)
.navigationBarTitle("Root")
}
}
}


struct ContentView2: View {
@Binding var rootIsActive : Bool


var body: some View {
NavigationLink(destination: ContentView3(shouldPopToRootView: self.$rootIsActive)) {
Text("Hello, World #2!")
}
.isDetailLink(false)
.navigationBarTitle("Two")
}
}


struct ContentView3: View {
@Binding var shouldPopToRootView : Bool


var body: some View {
VStack {
Text("Hello, World #3!")
Button (action: { self.shouldPopToRootView = false } ){
Text("Pop to root")
}
}.navigationBarTitle("Three")
}
}


struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Screen capture

对我来说,为了实现对 SwiftUI 中仍然缺失的导航的完全控制,我只是在 UINavigationController中嵌入了 SwiftUI 视图。在 SceneDelegate里面。请注意,为了使用 NavigationView 作为显示,我隐藏了导航栏。

class SceneDelegate: UIResponder, UIWindowSceneDelegate {


var window: UIWindow?


func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {


UINavigationBar.appearance().tintColor = .black


let contentView = OnBoardingView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let hostingVC = UIHostingController(rootView: contentView)
let mainNavVC = UINavigationController(rootViewController: hostingVC)
mainNavVC.navigationBar.isHidden = true
window.rootViewController = mainNavVC
self.window = window
window.makeKeyAndVisible()
}
}
}


然后我创建了这个协议和扩展,HasRootNavigationController

import SwiftUI
import UIKit


protocol HasRootNavigationController {
var rootVC:UINavigationController? { get }


func push<Content:View>(view: Content, animated:Bool)
func setRootNavigation<Content:View>(views:[Content], animated:Bool)
func pop(animated: Bool)
func popToRoot(animated: Bool)
}


extension HasRootNavigationController where Self:View {


var rootVC:UINavigationController? {
guard let scene = UIApplication.shared.connectedScenes.first,
let sceneDelegate = scene as? UIWindowScene,
let rootvc = sceneDelegate.windows.first?.rootViewController
as? UINavigationController else { return nil }
return rootvc
}


func push<Content:View>(view: Content, animated:Bool = true) {
rootVC?.pushViewController(UIHostingController(rootView: view), animated: animated)
}


func setRootNavigation<Content:View>(views: [Content], animated:Bool = true) {
let controllers =  views.compactMap { UIHostingController(rootView: $0) }
rootVC?.setViewControllers(controllers, animated: animated)
}


func pop(animated:Bool = true) {
rootVC?.popViewController(animated: animated)
}


func popToRoot(animated: Bool = true) {
rootVC?.popToRootViewController(animated: animated)
}
}


之后,在我的 SwiftUI 视图中,我使用/实现了 HasRootNavigationController协议和扩展

extension YouSwiftUIView:HasRootNavigationController {


func switchToMainScreen() {
self.setRootNavigation(views: [MainView()])
}


func pushToMainScreen() {
self.push(view: [MainView()])
}


func goBack() {
self.pop()
}


func showTheInitialView() {
self.popToRoot()
}
}

这里是我的代码的要点,以防我有一些更新

我最近创建了一个名为 快速导航堆栈的开源项目。它是 SwiftUI 的另一个导航栈。看一下 README 中的所有细节; 它真的很容易使用。

首先,如果你想在屏幕之间导航(例如,全屏视图) ,定义你自己的简单 Screen视图:

struct Screen<Content>: View where Content: View {
let myAppBackgroundColour = Color.white
let content: () -> Content


var body: some View {
ZStack {
myAppBackgroundColour.edgesIgnoringSafeArea(.all)
content()
}
}
}

然后将根嵌入到 NavigationStackView中(就像使用标准的 NavigationView那样) :

struct RootView: View {
var body: some View {
NavigationStackView {
Homepage()
}
}
}

现在让我们创建几个子视图来向您展示基本行为:

struct Homepage: View {
var body: some View {
Screen {
PushView(destination: FirstChild()) {
Text("PUSH FORWARD")
}
}
}
}


struct FirstChild: View {
var body: some View {
Screen {
VStack {
PopView {
Text("JUST POP")
}
PushView(destination: SecondChild()) {
Text("PUSH FORWARD")
}
}
}
}
}


struct SecondChild: View {
var body: some View {
Screen {
VStack {
PopView {
Text("JUST POP")
}
PopView(destination: .root) {
Text("POP TO ROOT")
}
}
}
}
}

您可以利用 PushViewPopView来来回回地导航。当然,您在 SceneDelegate中的内容视图必须是:

// Create the SwiftUI view that provides the window contents.
let contentView = RootView()

结果是:

Enter image description here

多亏了 Malhal 的@Binding 解决方案,我才知道我错过了 .isDetailLink(false)修饰符。

在我的例子中,我不想在每个后续视图中都使用@Binding。

这是我使用 Environment Object 的解决方案。

步骤1: 创建一个 AppState观察对象

import SwiftUI
import Combine


class AppState: ObservableObject {
@Published var moveToDashboard: Bool = false
}

步骤2: 创建 AppState的实例,并在 < strong > SceneCommittee 中添加 contentView

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
let appState = AppState()


// Use a UIHostingController as window root view controller.
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView:
contentView
.environmentObject(appState)
)
self.window = window
window.makeKeyAndVisible()
}
}

步骤3: 代码 ContentView.swift

我正在更新堆栈中最后一个视图的 appState值,这个值使用我在 contentView 中捕获的 .onReceive()来为 NavigationLink 将 isActive更新为 false。

这里的关键是在 NavigationLink 中使用 .isDetailLink(false),否则它将无法工作。

import SwiftUI
import Combine


class AppState: ObservableObject {
@Published var moveToDashboard: Bool = false
}


struct ContentView: View {
@EnvironmentObject var appState: AppState
@State var isView1Active: Bool = false


var body: some View {
NavigationView {
VStack {
Text("Content View")
.font(.headline)


NavigationLink(destination: View1(), isActive: $isView1Active) {
Text("View 1")
.font(.headline)
}
.isDetailLink(false)
}
.onReceive(self.appState.$moveToDashboard) { moveToDashboard in
if moveToDashboard {
print("Move to dashboard: \(moveToDashboard)")
self.isView1Active = false
self.appState.moveToDashboard = false
}
}
}
}
}


// MARK:- View 1
struct View1: View {


var body: some View {
VStack {
Text("View 1")
.font(.headline)
NavigationLink(destination: View2()) {
Text("View 2")
.font(.headline)
}
}
}
}


// MARK:- View 2
struct View2: View {
@EnvironmentObject var appState: AppState


var body: some View {
VStack {
Text("View 2")
.font(.headline)
Button(action: {
self.appState.moveToDashboard = true
}) {
Text("Move to Dashboard")
.font(.headline)
}
}
}
}




struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Enter image description here

当然,Malhal 有是解决方案的关键,但是对我来说,将绑定作为参数传递给视图是不切实际的。环境是一个更好的方式作为 伊姆萨特指出

这里是另一种模仿苹果公司发布的 disseid()方法的方法,以弹出到先前的视图。

定义对环境的扩展:

struct RootPresentationModeKey: EnvironmentKey {
static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}


extension EnvironmentValues {
var rootPresentationMode: Binding<RootPresentationMode> {
get { return self[RootPresentationModeKey.self] }
set { self[RootPresentationModeKey.self] = newValue }
}
}


typealias RootPresentationMode = Bool


extension RootPresentationMode {
    

public mutating func dismiss() {
self.toggle()
}
}

用法:

  1. .environment(\.rootPresentationMode, self.$isPresented)添加到根 NavigationView,其中 isPresented是用于显示第一个子视图的 Bool

  2. 要么将 .navigationViewStyle(StackNavigationViewStyle())修饰符添加到根 NavigationView,要么将 .isDetailLink(false)添加到第一个子视图的 NavigationLink

  3. @Environment(\.rootPresentationMode) private var rootPresentationMode添加到任何子视图中,从该视图中执行 pop 到 root 操作。

  4. 最后,从该子视图调用 self.rootPresentationMode.wrappedValue.dismiss()将弹出到根视图。

我出版了 GitHub 上的一个完整的工作示例

介绍苹果公司解决这个问题的方案

它也通过 HackingWithSwift (我是从它那里偷来的,LOL)呈现给你 在程式导航下:

(在 Xcode 12和 iOS 14上测试)

实际上,在 navigationlink中使用 tagselection可以直接进入任何您想要的页面。

struct ContentView: View {
@State private var selection: String? = nil


var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $selection) { EmptyView() }
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $selection) { EmptyView() }
Button("Tap to show second") {
self.selection = "Second"
}
Button("Tap to show third") {
self.selection = "Third"
}
}
.navigationBarTitle("Navigation")
}
}
}

您可以使用注入到 ContentView()中的 @environmentobject来处理选择:

class NavigationHelper: ObservableObject {
@Published var selection: String? = nil
}

注入应用程序:

@main
struct YourApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(NavigationHelper())
}
}
}

并使用它:

struct ContentView: View {
@EnvironmentObject var navigationHelper: NavigationHelper


var body: some View {
NavigationView {
VStack {
NavigationLink(destination: Text("Second View"), tag: "Second", selection: $navigationHelper.selection) { EmptyView() }
NavigationLink(destination: Text("Third View"), tag: "Third", selection: $navigationHelper.selection) { EmptyView() }
Button("Tap to show second") {
self.navigationHelper.selection = "Second"
}
Button("Tap to show third") {
self.navigationHelper.selection = "Third"
}
}
.navigationBarTitle("Navigation")
}
}
}

要返回到子导航链接中的 contentview,只需设置 navigationHelper.selection = nil

注意,如果你不想的话,你甚至不需要为后续的子导航链接使用标签和选择ーー不过它们不具备进入特定导航链接的功能。

这个解决方案是基于 Malhal 的回答,使用 Imthath 的建议弗洛林 · 小田,并要求保罗哈德森的导航视频把它一起为我。

这个想法很简单。在点击时,导航链接的 isActive 参数设置为 true。这允许显示第二个视图。可以使用其他链接添加更多视图。要返回根目录,只需将 isActive 设置为 false。第二种观点,加上其他可能堆积起来的观点,都消失了。

import SwiftUI


class Views: ObservableObject {
@Published var stacked = false
}


struct ContentView: View {
@ObservedObject var views = Views()


var body: some View {
NavigationView {
NavigationLink(destination: ContentView2(), isActive: self.$views.stacked) {
Text("Go to View 2") // Tapping this link sets stacked to true
}
.isDetailLink(false)
.navigationBarTitle("ContentView")
}
.environmentObject(views) // Inject a new views instance into the navigation view environment so that it's available to all views presented by the navigation view.
}
}


struct ContentView2: View {


var body: some View {
NavigationLink(destination: ContentView3()) {
Text("Go to View 3")
}
.isDetailLink(false)
.navigationBarTitle("View 2")
}
}


struct ContentView3: View {
@EnvironmentObject var views: Views


var body: some View {


Button("Pop to root") {
self.views.stacked = false // By setting this to false, the second view that was active is no more. Which means, the content view is being shown once again.
}
.navigationBarTitle("View 3")
}
}

这里有一个用于复杂导航的通用方法,它结合了这里描述的许多方法。如果有许多流需要返回根目录,而不仅仅是一个,那么这种模式非常有用。

首先,设置环境 Observer ableObject,为了便于阅读,使用枚举键入视图。

class ActiveView : ObservableObject {
@Published var selection: AppView? = nil
}


enum AppView : Comparable {
case Main, Screen_11, Screen_12, Screen_21, Screen_22
}


[...]
let activeView = ActiveView()
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(activeView))


在主 ContentView 中,使用 EmptyView ()上的 NavigationLink 按钮。我们这样做是为了使用 NavigationLink 的 isActive 参数而不是标记和选择。主视图上的 Screen _ 11需要在 Screen _ 12上保持活动状态,反之,Screen _ 21需要在 Screen _ 22上保持活动状态,否则视图将弹出。不要忘记将 isDetailLink 设置为 false。

struct ContentView: View {
@EnvironmentObject private var activeView: ActiveView


var body: some View {
NavigationView {
VStack {


// These buttons navigate by setting the environment variable.
Button(action: { self.activeView.selection = AppView.Screen_1.1}) {
Text("Navigate to Screen 1.1")
}


Button(action: { self.activeView.selection = AppView.Screen_2.1}) {
Text("Navigate to Screen 2.1")
}


// These are the navigation link bound to empty views so invisible
NavigationLink(
destination: Screen_11(),
isActive: orBinding(b: self.$activeView.selection, value1: AppView.Screen_11, value2: AppView.Screen_12)) {
EmptyView()
}.isDetailLink(false)


NavigationLink(
destination: Screen_21(),
isActive: orBinding(b: self.$activeView.selection, value1: AppView.Screen_21, value2: AppView.Screen_22)) {
EmptyView()
}.isDetailLink(false)
}
}
}

可以在 Screen _ 11上使用相同的模式导航到 Screen _ 12。

现在,这个复杂导航的突破点是 orBinding。它允许导航流上的视图堆栈保持活动状态。无论您是在 Screen _ 11还是 Screen _ 12上,都需要 NavigationLink (Screen _ 11)保持活动状态。

// This function create a new Binding<Bool> compatible with NavigationLink.isActive
func orBinding<T:Comparable>(b: Binding<T?>, value1: T, value2: T) -> Binding<Bool> {
return Binding<Bool>(
get: {
return (b.wrappedValue == value1) || (b.wrappedValue == value2)
},
set: { newValue in  } // Don't care the set
)
}

我想出了一个简单的解决方案来弹出到根视图。我正在发送一个通知,然后侦听更改 NavigationView 的 id 的通知; 这将刷新 NavigationView。没有动画,但看起来不错。下面是一个例子:

@main
struct SampleApp: App {
@State private var navigationId = UUID()


var body: some Scene {
WindowGroup {
NavigationView {
Screen1()
}
.id(navigationId)
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("popToRootView"))) { output in
navigationId = UUID()
}
}
}
}


struct Screen1: View {
var body: some View {
VStack {
Text("This is screen 1")
NavigationLink("Show Screen 2", destination: Screen2())
}
}
}


struct Screen2: View {
var body: some View {
VStack {
Text("This is screen 2")
Button("Go to Home") {
NotificationCenter.default.post(name: Notification.Name("popToRootView"), object: nil)
}
}
}
}

更容易显示和取消包含 NavigationView 的模态视图控制器。将模态视图控制器设置为全屏,然后将其取消,所产生的效果与弹出到 root 的导航视图堆栈相同。

例如 如何使用 fullScreencover ()呈现全屏模式视图

由于目前 SwiftUI 仍然在后台使用 UINavigationController,因此也可以调用它的 popToRootViewController(animated:)函数。您只需像下面这样搜索 UINavigationController 的视图控制器层次结构:

struct NavigationUtil {
static func popToRootView() {
findNavigationController(viewController: UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController)?
.popToRootViewController(animated: true)
}


static func findNavigationController(viewController: UIViewController?) -> UINavigationController? {
guard let viewController = viewController else {
return nil
}


if let navigationController = viewController as? UINavigationController {
return navigationController
}


for childViewController in viewController.children {
return findNavigationController(viewController: childViewController)
}


return nil
}
}

像这样使用它:

struct ContentView: View {
var body: some View {
NavigationView { DummyView(number: 1) }
}
}


struct DummyView: View {
let number: Int


var body: some View {
VStack(spacing: 10) {
Text("This is view \(number)")
NavigationLink(destination: DummyView(number: number + 1)) {
Text("Go to view \(number + 1)")
}
Button(action: { NavigationUtil.popToRootView() }) {
Text("Or go to root view!")
}
}
}
}

这是我的解决方案。 IT 在任何地方都可以工作,没有依赖性。

let window = UIApplication.shared.connectedScenes
.filter { $0.activationState == .foregroundActive }
.map { $0 as? UIWindowScene }
.compactMap { $0 }
.first?.windows
.filter { $0.isKeyWindow }
.first
let nvc = window?.rootViewController?.children.first as? UINavigationController
nvc?.popToRootViewController(animated: true)

我找到了一个对我很有效的解决方案,它是这样运作的:

GIF 图像显示了它是如何工作的

ContentView.swift文件中:

  1. 定义一个 RootSelection类,声明一个 @EnvironmentObjectRootSelection,仅在根视图中记录当前活动 NavigationLink的标记。
  2. 给每个 NavigationLink添加一个修饰符 .isDetailLink(false),这不是最终的细节视图。
  3. 使用文件系统层次结构来模拟 NavigationView
  4. 当根视图具有多个 NavigationLink时,该解决方案可以很好地工作。
import SwiftUI


struct ContentView: View {
var body: some View {
NavigationView {
SubView(folder: rootFolder)
}
}
}


struct SubView: View {
@EnvironmentObject var rootSelection: RootSelection
var folder: Folder


var body: some View {
List(self.folder.documents) { item in
if self.folder.documents.count == 0 {
Text("empty folder")
} else {
if self.folder.id == rootFolder.id {
NavigationLink(item.name, destination: SubView(folder: item as! Folder), tag: item.id, selection: self.$rootSelection.tag)
.isDetailLink(false)
} else {
NavigationLink(item.name, destination: SubView(folder: item as! Folder))
.isDetailLink(false)
}
}
}
.navigationBarTitle(self.folder.name, displayMode: .large)
.listStyle(SidebarListStyle())
.overlay(
Button(action: {
rootSelection.tag = nil
}, label: {
Text("back to root")
})
.disabled(self.folder.id == rootFolder.id)
)
}
}


struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(RootSelection())
}
}


class RootSelection: ObservableObject {
@Published var tag: UUID? = nil
}


class Document: Identifiable {
let id = UUID()
var name: String


init(name: String) {
self.name = name
}
}


class File: Document {}


class Folder: Document {
var documents: [Document]


init(name: String, documents: [Document]) {
self.documents = documents
super.init(name: name)
}
}


let rootFolder = Folder(name: "root", documents: [
Folder(name: "folder1", documents: [
Folder(name: "folder1.1", documents: []),
Folder(name: "folder1.2", documents: []),
]),
Folder(name: "folder2", documents: [
Folder(name: "folder2.1", documents: []),
Folder(name: "folder2.2", documents: []),
])
])

xxxApp.swift文件中的 ContentView()对象需要 .environmentObject(RootSelection())

import SwiftUI


@main
struct DraftApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(RootSelection())
}
}
}

很简单。 在根视图中(您希望返回的位置)使用 isActive 设计器的 NavigationLink 就足够了。在最后一个视图中,切换到控制 isActive 参数的 FALSE 变量。

在 Swift 5.5版本中,可以选择使用. isDetaillink (false)。

您可以像我在示例中那样使用一些公共类,或者通过绑定将该变量沿着 VIEW 层次结构传输。使用对你更方便的方式。

class ViewModel: ObservableObject {
@Published var isActivate = false
}


@main
struct TestPopToRootApp: App {
let vm = ViewModel()
    

var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(vm)
}
}
}


struct ContentView: View {
@EnvironmentObject var vm: ViewModel
    

var body: some View {
NavigationView {
NavigationLink("Go to view2", destination: NavView2(), isActive: $vm.isActivate)
.navigationTitle(Text("Root view"))
}
}
}


struct NavView2: View {
var body: some View {
NavigationLink("Go to view3", destination: NavView3())
.navigationTitle(Text("view2"))
}
}


struct NavView3: View {
@EnvironmentObject var vm: ViewModel
    

var body: some View {
Button {
vm.isActivate = false
} label: {
Text("Back to root")
}


.navigationTitle(Text("view3"))
}
}

导航视图工具包

import NavigationViewKit
NavigationView {
List(0..<10) { _ in
NavigationLink("abc", destination: DetailView())
}
}
.navigationViewManager(for: "nv1", afterBackDo: {print("back to root") })

在 NavigationView 的任何视图中:

@Environment(\.navigationManager) var nvmanager


Button("back to root view") {
nvmanager.wrappedValue.popToRoot(tag:"nv1") {
print("other back")
}
}

还可以通过 NotificationCenter 调用它,而不必在视图中调用它

let backToRootItem = NavigationViewManager.BackToRootItem(tag: "nv1", animated: false, action: {})
NotificationCenter.default.post(name: .NavigationViewManagerBackToRoot, object: backToRootItem)

在 iOS15中有一个简单的解决方案,那就是使用 release () ,然后传递到 subview:

struct ContentView: View {
@State private var showingSheet = false
var body: some View {
NavigationView {
Button("show sheet", action: { showingSheet.toggle()})
.navigationTitle("ContentView")
}.sheet(isPresented: $showingSheet) { FirstSheetView() }
}
}


struct FirstSheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationView {
List {
NavigationLink(destination: SecondSheetView(dismiss: _dismiss)) {
Text("show 2nd Sheet view")
}
NavigationLink(destination: ThirdSheetView(dismiss: _dismiss)) {
Text("show 3rd Sheet view")
}
Button("cancel", action: {dismiss()})
} .navigationTitle("1. SheetView")
}
}
}


struct SecondSheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
List {
NavigationLink(destination: ThirdSheetView(dismiss: _dismiss)) {
Text("show 3rd SheetView")
}
Button("cancel", action: {dismiss()})
} .navigationTitle("2. SheetView")
}
}


struct ThirdSheetView: View {
@Environment(\.dismiss) var dismiss
var body: some View {
List {
Button("cancel", action: {dismiss()})
} .navigationTitle("3. SheetView")
}
}

细节

  • Xcode Version 13.2.1(13C100) ,Swift 5.5

解决方案

联系名单

Https://github.com/raywenderlich/swift-algorithm-club/blob/master/linked%20list/linkedlist.swift

导航栈

import SwiftUI
import Combine


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MARK: Custom NavigationLink
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


final class CustomNavigationLinkViewModel<CustomViewID>: ObservableObject where CustomViewID: Equatable {
private weak var navigationStack: NavigationStack<CustomViewID>?
/// `viewId` is used to find a `CustomNavigationLinkViewModel` in the `NavigationStack`
let viewId = UUID().uuidString
  

/// `customId` is used to mark a `CustomNavigationLink` in the `NavigationStack`. This is kind of external id.
/// In `NavigationStack` we always prefer to use `viewId`. But from time to time we need to implement `pop several views`
/// and that is the purpose of the `customId`
/// Developer can just create a link with `customId` e.g. `navigationStack.navigationLink(customId: "123") { .. }`
/// And to pop directly to  view `"123"` should use `navigationStack.popToLast(customId: "123")`
let customId: CustomViewID?


@Published var isActive = false {
didSet { navigationStack?.updated(linkViewModel: self) }
}


init (navigationStack: NavigationStack<CustomViewID>, customId: CustomViewID? = nil) {
self.navigationStack = navigationStack
self.customId = customId
}
}


extension CustomNavigationLinkViewModel: Equatable {
static func == (lhs: CustomNavigationLinkViewModel, rhs: CustomNavigationLinkViewModel) -> Bool {
lhs.viewId == rhs.viewId && lhs.customId == rhs.customId
}
}


struct CustomNavigationLink<Label, Destination, CustomViewID>: View where Label: View, Destination: View, CustomViewID: Equatable {


/// Link `ViewModel` where all states are stored
@StateObject var viewModel: CustomNavigationLinkViewModel<CustomViewID>


let destination: () -> Destination
let label: () -> Label


var body: some View {
NavigationLink(isActive: $viewModel.isActive, destination: destination, label: label)
}
}


//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MARK: NavigationStack
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////


class NavigationStack<CustomViewID>: ObservableObject where CustomViewID: Equatable {
  

typealias Link = WeakReference<CustomNavigationLinkViewModel<CustomViewID>>
private var linkedList = LinkedList<Link>()


func navigationLink<Label, Destination>(customId: CustomViewID? = nil,
@ViewBuilder destination: @escaping () -> Destination,
@ViewBuilder label: @escaping () -> Label)
-> some View where Label: View, Destination: View {
createNavigationLink(customId: customId, destination: destination, label: label)
}
  

private func createNavigationLink<Label, Destination>(customId: CustomViewID? = nil,
@ViewBuilder destination: @escaping () -> Destination,
@ViewBuilder label: @escaping () -> Label)
-> CustomNavigationLink<Label, Destination, CustomViewID> where Label: View, Destination: View {
.init(viewModel: CustomNavigationLinkViewModel(navigationStack: self, customId: customId),
destination: destination,
label: label)
}
}


// MARK: Nested Types


extension NavigationStack {
/// To avoid retain cycle it is important to store weak reference to the `CustomNavigationLinkViewModel`
final class WeakReference<T> where T: AnyObject {
private(set) weak var weakReference: T?
init(value: T) { self.weakReference = value }
deinit { print("deinited WeakReference") }
}
}


// MARK: Searching


extension NavigationStack {
private func last(where condition: (Link) -> Bool) -> LinkedList<Link>.Node? {
var node = linkedList.last
while(node != nil) {
if let node = node, condition(node.value) {
return node
}
node = node?.previous
}
return nil
}
}


// MARK: Binding


extension NavigationStack {
fileprivate func updated(linkViewModel: CustomNavigationLinkViewModel<CustomViewID>) {
guard linkViewModel.isActive else {
switch linkedList.head?.value.weakReference {
case nil: break
case linkViewModel: linkedList.removeAll()
default:
last (where: { $0.weakReference === linkViewModel })?.previous?.next = nil
}
return
}
linkedList.append(WeakReference(value: linkViewModel))
}
}


// MARK: pop functionality


extension NavigationStack {
func popToRoot() {
linkedList.head?.value.weakReference?.isActive = false
}
  

func pop() {
linkedList.last?.value.weakReference?.isActive = false
}
  

func popToLast(customId: CustomViewID) {
last (where: { $0.weakReference?.customId == customId })?.value.weakReference?.isActive = false
}
}


#if DEBUG


extension NavigationStack {
var isEmpty: Bool { linkedList.isEmpty }
var count: Int { linkedList.count }
func testCreateNavigationLink<Label, Destination>(viewModel: CustomNavigationLinkViewModel<CustomViewID>,
@ViewBuilder destination: @escaping () -> Destination,
@ViewBuilder label: @escaping () -> Label)
-> CustomNavigationLink<Label, Destination, CustomViewID> where Label: View, Destination: View {
.init(viewModel: viewModel, destination: destination, label: label)
}
  

}
#endif

用法(短样本)

创建导航链接:

struct Page: View {
@EnvironmentObject var navigationStack: NavigationStack<String>
var body: some View {
navigationStack.navigationLink {
NextView(...)
} label: {
Text("Next page")
}
}
}

流行功能

struct Page: View {
@EnvironmentObject var navigationStack: NavigationStack<String>
var body: some View {
Button("Pop") {
navigationStack.pop()
}
Button("Pop to Page 1") {
navigationStack.popToLast(customId: "1")
}
Button("Pop to root") {
navigationStack.popToRoot()
}
}
}

用量(完整样本)

import SwiftUI


struct ContentView: View {
var body: some View {
TabView {
addTab(title: "Tab 1", systemImageName: "house")
addTab(title: "Tab 2", systemImageName: "bookmark")
}
}
  

func addTab(title: String, systemImageName: String) -> some View {
NavigationView {
RootPage(title: "\(title) home")
.navigationBarTitle(title)
}
.environmentObject(NavigationStack<String>())
.navigationViewStyle(StackNavigationViewStyle())
.tabItem {
Image(systemName: systemImageName)
Text(title)
}
}
}


struct RootPage: View {
let title: String
var body: some View {
SimplePage(title: title, pageCount: 0)
}
}


struct SimplePage: View {
@EnvironmentObject var navigationStack: NavigationStack<String>


var title: String
var pageCount: Int
var body: some View {
VStack {
navigationStack.navigationLink(customId: "\(pageCount)") {
// router.navigationLink {
SimplePage(title: "Page: \(pageCount + 1)", pageCount: pageCount + 1)
} label: {
Text("Next page")
}
Button("Pop") {
navigationStack.pop()
}
Button("Pop to Page 1") {
navigationStack.popToLast(customId: "1")
}
Button("Pop to root") {
navigationStack.popToRoot()
}
}
.navigationTitle(title)
}
}

一些单元测试

@testable import SwiftUIPop
import XCTest
import SwiftUI
import Combine


class SwiftUIPopTests: XCTestCase {
typealias CustomLinkID = String
typealias Stack = NavigationStack<CustomLinkID>
private let stack = Stack()
}


// MARK: Empty Navigation Stack


extension SwiftUIPopTests {
func testNoCrashOnPopToRootOnEmptyStack() {
XCTAssertTrue(stack.isEmpty)
stack.popToRoot()
}
  

func testNoCrashOnPopToLastOnEmptyStack() {
XCTAssertTrue(stack.isEmpty)
stack.popToLast(customId: "123")
}
  

func testNoCrashOnPopOnEmptyStack() {
XCTAssertTrue(stack.isEmpty)
stack.pop()
}
}


// MARK: expectation functions


private extension SwiftUIPopTests {
func navigationStackShould(beEmpty: Bool) {
if beEmpty {
XCTAssertTrue(stack.isEmpty, "Navigation Stack should be empty")
} else {
XCTAssertFalse(stack.isEmpty, "Navigation Stack should not be empty")
}
}
}


// MARK: Data / model generators


private extension SwiftUIPopTests {
func createNavigationLink(viewModel: CustomNavigationLinkViewModel<CustomLinkID>, stack: Stack)
-> CustomNavigationLink<EmptyView, EmptyView, CustomLinkID> {
stack.testCreateNavigationLink(viewModel: viewModel) {
EmptyView()
} label: {
EmptyView()
}
}
  

func createNavigationLinkViewModel(customId: CustomLinkID? = nil) -> CustomNavigationLinkViewModel<CustomLinkID> {
.init(navigationStack: stack, customId: customId)
}
}


// MARK: test `isActive` changing from `true` to `false` on `pop`


extension SwiftUIPopTests {
private func isActiveChangeOnPop(customId: String? = nil,
popAction: (Stack) -> Void,
file: StaticString = #file,
line: UInt = #line) {
navigationStackShould(beEmpty: true)
let expec = expectation(description: "Wait for viewModel.isActive changing")
    

var canalables = Set<AnyCancellable>()
let viewModel = createNavigationLinkViewModel(customId: customId)
let navigationLink = createNavigationLink(viewModel: viewModel, stack: stack)
navigationLink.viewModel.isActive = true
navigationLink.viewModel.$isActive.dropFirst().sink { value in
expec.fulfill()
}.store(in: &canalables)
    

navigationStackShould(beEmpty: false)
popAction(stack)
waitForExpectations(timeout: 2)
navigationStackShould(beEmpty: true)
}
  

func testIsActiveChangeOnPop() {
isActiveChangeOnPop { $0.pop() }
}
  

func testIsActiveChangeOnPopToRoot() {
isActiveChangeOnPop { $0.popToRoot() }
}
  

func testIsActiveChangeOnPopToLast() {
let customId = "1234"
isActiveChangeOnPop(customId: customId) { $0.popToLast(customId: customId) }
}
  

func testIsActiveChangeOnPopToLast2() {
navigationStackShould(beEmpty: true)
let expec = expectation(description: "Wait")


var canalables = Set<AnyCancellable>()
let viewModel = createNavigationLinkViewModel(customId: "123")
let navigationLink = createNavigationLink(viewModel: viewModel, stack: stack)
navigationLink.viewModel.isActive = true
navigationLink.viewModel.$isActive.dropFirst().sink { value in
expec.fulfill()
}.store(in: &canalables)


navigationStackShould(beEmpty: false)
stack.popToLast(customId: "1234")
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1)) {
expec.fulfill()
}
waitForExpectations(timeout: 3)
navigationStackShould(beEmpty: false)
}
}


// MARK: Check that changing `CustomNavigationLinkViewModel.isActive` will update `Navigation Stack`


extension SwiftUIPopTests {


// Add and remove view to the empty stack
private func isActiveChangeUpdatesNavigationStack1(createLink: (Stack) -> CustomNavigationLink<EmptyView, EmptyView, String>) {
navigationStackShould(beEmpty: true)
let navigationLink = createLink(stack)
navigationStackShould(beEmpty: true)
navigationLink.viewModel.isActive = true
navigationStackShould(beEmpty: false)
navigationLink.viewModel.isActive = false
navigationStackShould(beEmpty: true)
}


func testIsActiveChangeUpdatesNavigationStack1() {
isActiveChangeUpdatesNavigationStack1 { stack in
let viewModel = createNavigationLinkViewModel()
return createNavigationLink(viewModel: viewModel, stack: stack)
}
}


func testIsActiveChangeUpdatesNavigationStack2() {
isActiveChangeUpdatesNavigationStack1 { stack in
let viewModel = createNavigationLinkViewModel(customId: "123")
return createNavigationLink(viewModel: viewModel, stack: stack)
}
}


// Add and remove view to the non-empty stack
private func isActiveChangeUpdatesNavigationStack2(createLink: (Stack) -> CustomNavigationLink<EmptyView, EmptyView, String>) {
navigationStackShould(beEmpty: true)
let viewModel1 = createNavigationLinkViewModel()
let navigationLink1 = createNavigationLink(viewModel: viewModel1, stack: stack)
navigationLink1.viewModel.isActive = true
navigationStackShould(beEmpty: false)
XCTAssertEqual(stack.count, 1, "Navigation Stack Should contains only one link")


let navigationLink2 = createLink(stack)
navigationLink2.viewModel.isActive = true
navigationStackShould(beEmpty: false)
navigationLink2.viewModel.isActive = false
XCTAssertEqual(stack.count, 1, "Navigation Stack Should contains only one link")
}


func testIsActiveChangeUpdatesNavigationStack3() {
isActiveChangeUpdatesNavigationStack2 { stack in
let viewModel = createNavigationLinkViewModel()
return createNavigationLink(viewModel: viewModel, stack: stack)
}
}


func testIsActiveChangeUpdatesNavigationStack4() {
isActiveChangeUpdatesNavigationStack2 { stack in
let viewModel = createNavigationLinkViewModel(customId: "123")
return createNavigationLink(viewModel: viewModel, stack: stack)
}
}
}

马尔哈尔的回答绝对是正确的。 我做了一个包装到 NavigationLink,允许我应用任何修饰我需要除了 isDetailLink(false)之一,并捕获任何数据我需要的修饰。

具体来说,它捕获 isActive绑定或 tag绑定,这样当我想要弹出到声明自己为根的任何视图时,就可以重置这些绑定。

设置 isRoot = true将存储该视图的绑定,并且 dismiss参数接受一个可选的闭包,以防在弹出发生时需要执行某些操作。

我从 SwiftUI NavigationLinks 初始化器中复制了基本签名,用于简单的布尔导航或基于标记的导航,以便于编辑现有用法。如果需要,可以直接添加其他内容。

包装纸是这样的:

struct NavigationStackLink<Label, Destination> : View where Label : View, Destination : View {
var isActive: Binding<Bool>? // Optionality implies whether tag or Bool binding is used
var isRoot: Bool = false
let link: NavigationLink<Label, Destination>


private var dismisser: () -> Void = {}


/// Wraps [NavigationLink](https://developer.apple.com/documentation/swiftui/navigationlink/init(isactive:destination:label:))
/// `init(isActive: Binding<Bool>, destination: () -> Destination, label: () -> Label)`
/// - Parameters:
///     - isActive:  A Boolean binding controlling the presentation state of the destination
///     - isRoot: Indicate if this is the root view. Used to pop to root level. Default `false`
///     - dismiss: A closure that is called when the link destination is about to be dismissed
///     - destination: The link destination view
///     - label: The links label
init(isActive: Binding<Bool>, isRoot : Bool = false, dismiss: @escaping () -> Void = {}, @ViewBuilder destination: @escaping () -> Destination, @ViewBuilder label: @escaping () -> Label) {
self.isActive = isActive
self.isRoot = isRoot
self.link = NavigationLink(isActive: isActive, destination: destination, label: label)
self.dismisser = dismiss
}


/// Wraps [NavigationLink ](https://developer.apple.com/documentation/swiftui/navigationlink/init(tag:selection:destination:label:))
init<V>(tag: V, selection: Binding<V?>, isRoot : Bool = false, dismiss: @escaping () -> Void = {}, @ViewBuilder destination: @escaping () -> Destination, @ViewBuilder label: @escaping () -> Label) where V : Hashable
{
self.isRoot = isRoot
self.link = NavigationLink(tag: tag, selection: selection, destination: destination, label: label)
self.dismisser = dismiss
self.isActive = Binding (get: {
selection.wrappedValue == tag
}, set: { newValue in
if newValue {
selection.wrappedValue = tag
} else {
selection.wrappedValue = nil
}
})
}


// Make sure you inject your external store into your view hierarchy
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
// Store whatever you need to in your external object
if isRoot {
viewRouter.root = isActive
}
viewRouter.dismissals.append(self.dismisser)
// Return the link with whatever modification you need
return link
.isDetailLink(false)
}
}

ViewRouter可以是任何你需要的。我使用了一个 ObservableObject,目的是最终为将来更复杂的堆栈操作添加一些 Published值:

class ViewRouter: ObservableObject {


var root: Binding<Bool>?
typealias Dismiss = () -> Void
var dismissals : [Dismiss] = []


func popToRoot() {
dismissals.forEach { dismiss in
dismiss()
}
dismissals = []
root?.wrappedValue = false
}
}

起初,我使用的是来自 Chuck H的解决方案,该解决方案发布在 给你上。

但是我遇到了一个问题,这个解决方案在我的情况下不起作用。当根视图是两个或多个流的起点,并且在这些流的某个点上,用户有能力执行 pop to root时,它与这种情况相连。在这种情况下,这个解决方案不工作,因为它有一个共同的状态 @Environment(\.rootPresentationMode) private var rootPresentationMode

我使用附加枚举 Route制作了 RouteManager,它描述了用户能够执行 pop to root的一些特定流程

路线管理器:

final class RouteManager: ObservableObject {
@Published
private var routers: [Int: Route] = [:]


subscript(for route: Route) -> Route? {
get {
routers[route.rawValue]
}
set {
routers[route.rawValue] = route
}
}


func select(_ route: Route) {
routers[route.rawValue] = route
}


func unselect(_ route: Route) {
routers[route.rawValue] = nil
}
}

路线:

enum Route: Int, Hashable {
case signUp
case restorePassword
case orderDetails
}

用法:

struct ContentView: View {
@EnvironmentObject
var routeManager: RouteManager


var body: some View {
NavigationView {
VStack {
NavigationLink(
destination: SignUp(),
tag: .signUp,
selection: $routeManager[for: .signUp]
) { EmptyView() }.isDetailLink(false)
NavigationLink(
destination: RestorePassword(),
tag: .restorePassword,
selection: $routeManager[for: .restorePassword]
) { EmptyView() }.isDetailLink(false)
Button("Sign Up") {
routeManager.select(.signUp)
}
Button("Restore Password") {
routeManager.select(.restorePassword)
}
}
.navigationBarTitle("Navigation")
.onAppear {
routeManager.unselect(.signUp)
routeManager.unselect(.restorePassword)
}
}.navigationViewStyle(StackNavigationViewStyle())
}
}

重要!

当用户前进到流,然后通过点击后退按钮返回时,您应该使用 RouteManagerunselect方法。在这种情况下,需要为以前选择的流重置路由管理器的状态,以避免未定义(意外)行为:

.onAppear {
routeManager.unselect(.signUp)
routeManager.unselect(.restorePassword)
}

Demo

您可以找到一个完整的演示项目 给你

要不使用 .isDetailLink(false)转到 Root View,您需要从 Root View的层次结构视图中删除 NavigationLink

class NavigationLinkStore: ObservableObject {
static let shared = NavigationLinkStore()


@Published var showLink = false
}


struct NavigationLinkView: View {
@ObservedObject var store = NavigationLinkStore.shared
@State var isActive = false


var body: some View {
NavigationView {
VStack {
Text("Main")


Button("Go to View1") {
Task {
store.showLink = true
try await Task.sleep(seconds: 0.1)
isActive = true
}
}


if store.showLink {
NavigationLink(
isActive: $isActive,
destination: { NavigationLink1View() },
label: { EmptyView() }
)
}
}
}
}
}


struct NavigationLink1View: View {
var body: some View {
VStack {
Text("View1")
NavigationLink("Go to View 2", destination: NavigationLink2View())
}
}
}


struct NavigationLink2View: View {
@ObservedObject var store = NavigationLinkStore.shared


var body: some View {
VStack {
Text("View2")
Button("Go to root") {
store.showLink = false
}
}
}
}

我还没有在 SwiftUI 中找到解决方案,但是我找到了 库 清洁用户界面(CleanUI)

使用 导航类,我可以实现我想要的导航模式。

图书馆的 README 中的一个例子:

NavigationView {
Button(action: {
CUNavigation.pushToSwiftUiView(YOUR_VIEW_HERE)
}){
Text("Push To SwiftUI View")
}


Button(action: {
CUNavigation.popToRootView()
}){
Text("Pop to the Root View")
}


Button(action: {
CUNavigation.pushBottomSheet(YOUR_VIEW_HERE)
}){
Text("Push to a Botton-Sheet")
}
}

使用 NavigationViewNavigationLink很难做到这一点。但是,如果您使用的是 UIPilot库,它是 NavigationView周围的一个小包装器,那么弹出到任何目的地都是非常简单的。

假设你有路线,

enum AppRoute: Equatable {
case Home
case Detail
case NestedDetail
}

你可以像下面这样设置根视图

struct ContentView: View {
@StateObject var pilot = UIPilot(initial: AppRoute.Home)


var body: some View {
UIPilotHost(pilot)  { route in
switch route {
case .Home: return AnyView(HomeView())
case .Detail: return AnyView(DetailView())
case .NestedDetail: return AnyView(NestedDetail())
}
}
}
}

你想从 NestedDetail屏幕弹出到 Home,只需使用 popTo函数。

struct NestedDetail: View {
@EnvironmentObject var pilot: UIPilot<AppRoute>


var body: some View {
VStack {
Button("Go to home", action: {
pilot.popTo(.Home)   // Pop to home
})
}.navigationTitle("Nested detail")
}
}

我创建了一个解决方案,“只是工作”,并与它非常高兴。要使用我的魔法解决方案,只有几个步骤,你必须做的。

它首先使用 rootPresentationMode,这是在这个线程的其他地方使用的:

// Create a custom environment key
struct RootPresentationModeKey: EnvironmentKey {
static let defaultValue: Binding<RootPresentationMode> = .constant(RootPresentationMode())
}


extension EnvironmentValues {
var rootPresentationMode: Binding<RootPresentationMode> {
get { self[RootPresentationModeKey.self] }
set { self[RootPresentationModeKey.self] = newValue }
}
}


typealias RootPresentationMode = Bool


extension RootPresentationMode: Equatable {
mutating func dismiss() {
toggle()
}
}

接下来是魔法,它有两个步骤。

  1. 创建一个视图修饰符,用于监视对 rootPresentationMode变量的更改。

    struct WithRoot: ViewModifier {
    @Environment(\.rootPresentationMode) private var rootPresentationMode
    @Binding var rootBinding: Bool
    
    
    func body(content: Content) -> some View {
    content
    .onChange(of: rootBinding) { newValue in
    // We only care if it's set to true
    if newValue {
    rootPresentationMode.wrappedValue = true
    }
    }
    .onChange(of: rootPresentationMode.wrappedValue) { newValue in
    // We only care if it's set to false
    if !newValue {
    rootBinding = false
    }
    }
    }
    }
    
    
    extension View {
    func withRoot(rootBinding: Binding<Bool>) -> some View {
    modifier(WithRoot(rootBinding: rootBinding))
    }
    }
    
  2. 向所有导航视图添加 isPresented

    struct ContentView: View {
    // This seems.. unimportant, but it's crucial. This variable
    // lets us pop back to the root view from anywhere by adding
    // a withRoot() modifier
    // It's only used indirectly by the withRoot() modifier.
    @State private var isPresented = false
    
    
    var body: some View {
    NavigationView {
    MyMoneyMakingApp()
    }
    // rootPresentationMode MUST be set on a NavigationView to be
    // accessible from everywhere
    .environment(\.rootPresentationMode, $isPresented)
    }
    

要在(任何)子视图中使用它,您所要做的就是

struct MyMoneyMakingApp: View {
@State private var isActive = false


var body: some View {
VStack {
NavigationLink(destination: ADeepDeepLink(), isActive: $isActive) {
Text("go deep")
}
}
.withRoot(rootBinding: $isActive)
}
}


struct ADeepDeepLink: View {
@Environment(\.rootPresentationMode) private var rootPresentationMode


var body: some View {
VStack {
NavigationLink(destination: ADeepDeepLink()) {
Text("go deeper")
}
Button(action: {
rootPresentationMode.wrappedValue.dismiss()
}) {
Text("pop to root")
}
}
}
}

这是对 X0randgat3的回答的一个更新,它适用于 TabView中的多个 NavigationViews

struct NavigationUtil {
static func popToRootView() {
findNavigationController(viewController: UIApplication.shared.windows.filter { $0.isKeyWindow }.first?.rootViewController)?
.popToRootViewController(animated: true)
}


static func findNavigationController(viewController: UIViewController?) -> UINavigationController? {
guard let viewController = viewController else {
return nil
}


if let navigationController = viewController as? UITabBarController {
return findNavigationController(viewController: navigationController.selectedViewController)
}


if let navigationController = viewController as? UINavigationController {
return navigationController
}


for childViewController in viewController.children {
return findNavigationController(viewController: childViewController)
}


return nil
}
}

来自 @ malhal的答案确实有帮助,但在我的情况下,我需要的功能时,每个按钮被按下之前导航。如果你在同一条船上,试试这个代码!

//  ContentView.swift
//  Navigation View Buttons
//
//  Created by Jarren Campos on 9/10/22.
//


import SwiftUI


struct ContentView: View {


var body: some View{
VStack{
ContentView1()
}
}
}


struct ContentView1: View {
@State var isActive : Bool = false


var body: some View {
NavigationView {
VStack{
Button {
isActive = true
} label: {
Text("To 2")
}
}
.background{
NavigationLink(
destination: ContentView2(rootIsActive: self.$isActive),
isActive: self.$isActive) {}
.isDetailLink(false)
}
.navigationBarTitle("One")
}
}
}


struct ContentView2: View {
@Binding var rootIsActive : Bool
@State var toThirdView: Bool = false


var body: some View {


VStack{
Button {
toThirdView = true
} label: {
Text("to 3")
}
}
.background{
NavigationLink(isActive: $toThirdView) {
ContentView3(shouldPopToRootView: self.$rootIsActive)
} label: {}
.isDetailLink(false)
}
.navigationBarTitle("Two")


}
}


struct ContentView3: View {
@Binding var shouldPopToRootView : Bool


var body: some View {
VStack {
Text("Hello, World #3!")
Button {
self.shouldPopToRootView = false
} label: {
Text("Pop to root")
}
}
.navigationBarTitle("Three")
}
}

IOS 16解决方案

现在终于可以用新添加的 NavigationStack弹出到根视图了! ! !

struct DataObject: Identifiable, Hashable {
let id = UUID()
let name: String
}


@available(iOS 16.0, *)
struct ContentView8: View {
@State private var path = NavigationPath()
    

var body: some View {
NavigationStack(path: $path) {
Text("Root Pop")
.font(.largeTitle)
.foregroundColor(.primary)
            

NavigationLink("Click Item", value: DataObject.init(name: "Item"))
            

.listStyle(.plain)
.navigationDestination(for: DataObject.self) { course in
Text(course.name)
NavigationLink("Go Deeper", value: DataObject.init(name: "Item"))
Button("Back to root") {
path = NavigationPath()
}
}
}
.padding()
}
}