SwiftUI:如何弹出到根视图

SwiftUI: How to pop to Root view

现在终于有了 Beta 5,我们可以通过编程方式弹出到父视图。但是,在我的应用程序中有几个地方的视图有一个 "Save" 按钮,它结束了几个步骤的过程,returns 到开头。在 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()
        }
    }
}

我花了最后几个小时来尝试解决同样的问题。据我所知,使用当前的 beta 5 没有简单的方法。我发现的唯一方法非常 hacky 但有效。 基本上向您的 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 上仍然没有解决方案。

我找到了另一种回到根的方法,但这次我失去了动画,直接回到了根。 这个想法是强制刷新根视图,这样会导致导航堆栈的清理。

但最终只有 Apple 能够提供合适的解决方案,因为导航堆栈的管理在 SwiftUI 中不可用。

注意:下面通知的简单解决方案适用于 iOS 而不是 watchOS,因为 watchOS 在 2 个导航级别后从内存中清除根视图。但是让外部 class 管理 watchOS 的状态应该就可以了。

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

然后在委托上调用 public 函数。我想如果你做了类似的事情,保留了 View 但为它创建了一个新的 UIHostingController 并替换了 window.rootViewController 它可能对你有用。

我想出了另一种有效的方法,但仍然感觉很奇怪。它还会为两个屏幕关闭设置动画,但它 little 更干净。您可以 A) 将闭包向下传递到后续的详细信息屏幕或 B) 将 detailB 传递给 detailA 的 presentationMode。这两个都需要关闭 detailB,然后延迟一会儿,以便 detailA 在尝试关闭 detailA 之前回到屏幕上。

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。我已经添加了选项卡视图选项卡的选择和布尔值,说明是否显示特定视图

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.selection 绑定到 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)
        }
    }
}

例如,这是一个 navigationStacks

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,自适应包含View。例如,在 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()
    }
}

对我来说,为了实现对 swiftUI 中仍然缺少的导航的完全控制,我只是将 SwiftUI 视图嵌入到 UINavigationController 中。 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: [=11=]) }
        rootVC?.setViewControllers(controllers, animated: animated)
    }

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

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

在那之后,在我的 SwiftUI 视图上 used/implemented HasRootNavigationController 协议和扩展

extension YouSwiftUIView:HasRootNavigationController {

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

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

    func goBack() {
         self.pop()
    }

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

这是我的代码的要点,以防我有一些更新。 https://gist.github.com/michaelhenry/945fc63da49e960953b72bbc567458e6

我最近创建了一个名为 swiftui-navigation-stack (https://github.com/biobeats/swiftui-navigation-stack) 的开源项目。它是 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()

结果是:

感谢 "Malhal" 提供的@Binding 解决方案。我缺少 .isDetailLink(false) 修饰符。我从你的代码中学到的。

就我而言,我不想在每个后续视图中都使用@Binding。

所以这是我使用 EnvironmentObject 的解决方案。

第 1 步:创建一个 AppState ObservableObject

import SwiftUI
import Combine

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

第2步:创建AppState的实例并在SceneDelegate[=23=中添加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()
        }
    }

第三步:ContentView.swift的代码 所以我正在更新堆栈中最后一个视图的 appState 值,它使用 .onReceive() 我在 contentView 中捕获以将 NavigationLink 的 isActive 更新为 false。

此处的关键是将 .isDetailLink(false) 与 NavigationLink 结合使用。不然不行。

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()
    }
}

当然,@malhal 拥有解决方案的关键,但对我来说,将绑定作为参数传递到视图中是不切实际的。正如@Imthath 所指出的,环境是一种更好的方法。

这是另一种仿照 Apple 发布的 dismiss() 方法弹出到上一个视图的方法。

定义环境扩展:

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,其中isPresentedBool用来表示 第一个子视图。

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

  3. @Environment(\.rootPresentationMode) private var rootPresentationMode 添加到应该执行 pop 到 root 的任何子视图。

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

我已经在 GitHub 上发布了一个完整的工作示例:

https://github.com/Whiffer/SwiftUI-PopToRootExample

女士们先生们,介绍一下 Apple 解决这个问题的方法。 *也通过 HackingWithSwift 呈现给您(我从大声笑中偷走了这个):under programmatic navigation

(在 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")
    }
}
}

要返回子导航链接中的内容视图,只需设置 navigationHelper.selection = nil

请注意,如果您不想的话,您甚至不必为后续的子导航链接使用标签和选择 - 尽管它们没有转到该特定导航链接的功能。

此解决方案基于 malhal 的回答,使用了 Imthath 和 Florin Odagiu 的建议,并需要 Paul Hudson 的 NavigationView 视频为我将所有内容整合在一起。这个想法很简单。 navigationLink 的 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")
    }
}

这里是复杂导航的通用方法,它结合了此处描述的许多方法。如果您有许多流需要弹出回根而不只是一个,则此模式很有用。

首先,设置您的环境 ObservableObject 并为了可读性,使用枚举来键入您的视图。

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 的模态视图控制器更容易。将模态视图控制器设置为全屏然后关闭它会产生与弹出到根目录的导航视图堆栈相同的效果。

https://www.hackingwithswift.com/quick-start/swiftui/how-to-present-a-full-screen-modal-view-using-fullscreencover

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

struct NavigationUtil {
  static func popToRootView() {
    findNavigationController(viewController: UIApplication.shared.windows.filter { [=10=].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!")
      }
    }
  }
}

这是我的解决方案,可以在任何地方使用,没有依赖性。

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

我找到了适合我的解决方案。这是它的工作原理:

a gif shows how it works

ContentView.swift 文件中:

  1. 定义一个RootSelection class,声明一个RootSelection@EnvironmentObject,只在根视图中记录当前活动NavigationLink的标签。
  2. 为每个 NavigationLink 添加一个修饰符 .isDetailLink(false),这不是最终的详细视图。
  3. 使用文件系统层次结构来模拟 NavigationView
  4. 当根视图有多个 NavigationLink.
  5. 时,此解决方案工作正常
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: []),
    ])
])

.environmentObject(RootSelection()) 对于 xxxApp.swift 文件中的 ContentView() 对象是必需的

import SwiftUI

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

初级。 在根视图(您想返回的地方)中足够了,将 NavigationLink 与 isActive 设计器一起使用。在最后一个视图中,切换到控制 isActive 参数的 FALSE 变量。

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

您可以像我在示例中那样使用一些常见的 class,或者通过绑定将此变量向下传递到 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"))
    }
}

NavigationViewKit https://github.com/fatbobman/NavigationViewKit

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 中有一个简单的解决方案,即使用 dismiss() 并将 dismiss 传递给子视图:

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 版本 13.2.1 (13C100),Swift 5.5

解决方案

Linked list

https://github.com/raywenderlich/swift-algorithm-club/blob/master/Linked%20List/LinkedList.swift

NavigationStack

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: { [=10=].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: { [=10=].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 { [=14=].pop() }
  }
  
  func testIsActiveChangeOnPopToRoot() {
    isActiveChangeOnPop { [=14=].popToRoot() }
  }
  
  func testIsActiveChangeOnPopToLast() {
    let customId = "1234"
    isActiveChangeOnPop(customId: customId) { [=14=].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)
    }
  }
}

@malhal 的回答绝对是正确的。 我为 NavigationLink 制作了一个包装器,它允许我应用除 isDetailLink(false) 之外我需要的任何修饰符并捕获我需要的任何数据。 具体来说,它捕获 isActive 绑定或 tag 绑定,这样当我想弹出任何声明为根的视图时,我可以重置它们。 设置 isRoot = true 将存储该视图的绑定,并且 dismiss 参数采用可选的闭包,以防弹出发生时您需要完成某些操作。 我从 SwiftUI NavigationLink 的初始值设定项中复制了基本签名,用于简单的布尔值或基于标签的导航,以便轻松编辑现有用法。如果需要,添加其他人应该很简单。 包装器看起来像这样:

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 that was posted

中的解决方案

但是当这个解决方案对我的情况不起作用时,我遇到了一个问题。它与根视图是两个或多个流程的起点并且在这些流程的某个点用户能够执行 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)
}

您可以找到完整的演示项目here

要在不使用 .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 中找到解决方案,但我找到了这个库:https://github.com/knoggl/CleanUI

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

图书馆自述文件中的示例:

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 很难实现。但是,如果您正在使用 https://github.com/canopas/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))
    }
}
  1. 向所有 NavigationView 添加 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 { [=10=].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
  }
}