【问题标题】:SwiftUI: How to pop to Root viewSwiftUI:如何弹出到根视图
【发布时间】:2021-12-29 02:17:03
【问题描述】:

现在终于有了 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()
        }
    }
}

【问题讨论】:

标签: swift swiftui


【解决方案1】:

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

【讨论】:

  • 这是最好的答案,现在应该是公认的答案。它完全符合我的要求,而且不是 hack。谢谢。
  • 对于那些在你的视图上使用自定义初始化器并且无法让它们工作的人,确保你在你的初始化参数 "init(rootIsActive: Binding)" 上使用 Binding ,也在里面初始化器不要忘记使用下划线作为本地绑定变量(self._rootIsActive = rootIsActive)。当您的预览刹车时,只需使用 .constant(true) 作为参数。
  • 它可以工作,但“shouldPopToRootView”的命名不清楚。该属性有效地禁用了根视图上的导航。此外,最好使用环境对象来观察变化,而不是将绑定布尔值传递给子视图中的每个视图。
  • 如果根视图中有多个导航链接,那么这个解决方案可能会有点棘手。不要只为所有导航链接(在根视图中)提供相同的布尔绑定到 isActive。否则,当您导航时,所有导航链接将同时变为活动状态。棘手。
  • 感谢您的灵感和代码。我的两个关键美分: - ContentView 中不需要指令 .isDetailLink(false) (因为它是根视图)。 - 布尔值 rootIsActive 和 shouldPopToRootView 的命名非常非常糟糕。由于它们,我很难理解代码。尤其是 self.shouldPopToRootView = false 的东西看起来很诡异(假...?真的...?我们实际上是在尝试弹出到根视图,你知道...)。我所做的是用一个名为 stackingPermitted 的布尔值替换它们(连同 ContentView 中的 isActive)。
【解决方案2】:

当然,@malhal 拥有解决方案的关键,但对我来说,将 Binding 作为参数传递给 View 是不切实际的。正如@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 to root 的任何子视图。

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

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

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

【讨论】:

  • 这对我很有帮助。谢谢 Chuck 和 Nikola。
  • 这确实是一个优雅的、可重用的解决方案。我花了一些时间来理解它是如何工作的,但是由于你的例子,我明白了。任何尝试此操作的人:尝试根据您的需要最小化示例以便更好地理解。
  • 这是应该的。使用 Binding 与 DI 搭配不好,这很完美。
【解决方案3】:

由于目前 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!")
      }
    }
  }
}

【讨论】:

  • 为我工作!谢谢
  • 仍然有效。好吧,也许将来不会。但是现在为什么不能过上轻松的生活。感觉是最自然的方式。
  • 由于某种原因停止在这里工作...
【解决方案4】:

女士们,先生们,介绍 Apple 解决这个问题的方法。 *也通过 HackingWithSwift 呈现给你(我从 lol 偷来的):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

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

【讨论】:

  • 我面临的问题是,当我通过设置 navigationHelper.selection = nil 返回子导航链接中的 contentview 时,它不会延迟加载我的 ContentView。因此,变量不会根据子视图中生成的附加信息在 ContentView 中更新。关于如何解决这个问题的任何想法?
  • @JLively 可能只是在用户点击内容视图时手动重置数据?
  • 非常适合我。
  • 是的,这个解决方案确实有效。我刚刚找到了为什么它最初不起作用的答案。
  • @KenanKarakecili 是的,我不知道为什么会这样.. 但是在 child1 中删除 tag:selection: 会阻止它在弹出到 child2 时返回根目录 (nil)。但这意味着您将无法通过将 child2 的 tag 设置为 navigationHelper.selection 来访问 child2
【解决方案5】:

我花了最后几个小时试图解决同样的问题。据我所见,目前的 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 个导航级别后会从内存中清除根视图。但是有一个外部类来管理 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)
    }
}

【讨论】:

  • 谢谢。我很高兴看到它可以做到。你是对的,它有点hacky,但它确实有效。如果 DetailViewA 在返回 MasterView 的途中没有闪过,那将是最好的。我们可以希望 Apple 在即将到来的测试版中填补 SwiftUI 导航模型中的这一漏洞和其他一些漏洞。
【解决方案6】:

花了一些时间,但我想出了如何在 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.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)
        }
    }
}

例如,这是一个导航堆栈

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 状态。

【讨论】:

  • 创建一个 NavigationController 并将其放入 EnvironmentObject 是一个非常好的主意。我还没有完全让你的例子完全有效,但我认为它在正确的轨道上。谢谢。
  • 我意识到我还需要一个 var 来确保堆栈上的最后一个视图不会总是发生崩溃。我在这里添加了我的项目。 github.com/gahntpo/NavigationSwiftUI.git
  • 这是个好主意,但它在列表中如何工作?对我来说,列表中的每个项目都会打开一个详细视图,因为每个 NavigationLink 的 isActive 都设置为 true。
  • 如果你想使用列表,方法非常相似。我不会将 NavigationLink 放在 List 中(因为这会创建不同的链接,正如您所提到的)。您可以添加一个编程链接(意味着您没有可见的按钮)。 NavigationLink(目的地:MyView(数据:mySelectedDataFromTheList),isActive:$self.nav.isShown){ EmptyView()}。当用户在列表中的某个项目上进行选项卡时,您可以将 mySelectedDataFromTheList 设置为选项卡项目并将导航状态 isShown 更改为 true。
  • 我终于抽出时间写了一篇关于 SwiftUI 导航的博文。这对其进行了更多解释并显示了一些用例。 medium.com/@karinprater/…
【解决方案7】:

对我来说,为了实现对 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: $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()
    }
}

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

【讨论】:

  • 这是最适合我需求的解决方案,因为它使我能够以最小的更改实现当前的导航堆栈。可以进一步改进这一点的一个快速示例是在 gist 上使用它的导航堆栈,因为它需要一些思考才能使其工作(即必须调用 setRootNavigation 和何时)
  • 这个解决方案很棒,但是使用它我仍然没有找到实现NavigationView.navigationBarItems 修饰符的方法。我每次都必须修改 UINavigationBar。另外,您必须为您推送的每个视图传递 environmentObjects。
  • 出色的解决方案,有助于保持视图可重复使用,而无需传递不需要的参数。
  • 谢谢。 ?‍♂️
  • 谢谢。 Push 需要 View 而不是 View 的数组。所以self.push(view: [MainView()]) 应该是self.push(view: MainView())
【解决方案8】:

这是我使用 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()
    }
}

【讨论】:

  • 嗨@jpelayo,最喜欢你的解决方案。您可以删除大部分代码以使其更易于理解。棘手的部分只是检查所有中间视图的onAppear() 中的绑定标志。
【解决方案9】:

感谢“Malhal”为您提供@Binding 解决方案。我错过了 .isDetailLink(false) 修饰符。我从你的代码中学到的。

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

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

第 1 步:创建一个AppState ObservableObject

import SwiftUI
import Combine

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

第 2 步:创建 AppState 的实例并在 SceneDelegate

中添加 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的代码 因此,我正在更新 Stack 中最后一个视图的 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()
    }
}

【讨论】:

    【解决方案10】:

    导航视图套件 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)
    

    【讨论】:

      【解决方案11】:

      我最近创建了一个名为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()
      

      结果是:

      【讨论】:

        【解决方案12】:

        此解决方案基于 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")
            }
        }
        

        【讨论】:

          【解决方案13】:

          我想出了一个简单的解决方案来弹出到根视图。我正在发送通知,然后监听通知以更改 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)
                      }            
                  }
              }
          }
          

          【讨论】:

          • 古斯塔沃感谢您的回答。虽然这种技术可行,但它并不是与 SwiftUI 一起使用的最佳技术。 SwiftUI 的首选方法是使用 @State 变量来实现。
          【解决方案14】:

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

          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)
          

          【讨论】:

          • 正是我正在寻找的,非常感谢
          【解决方案15】:

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

          【讨论】:

          • 它不工作,根本不会解雇:(
          【解决方案16】:

          我没有完全同样的问题,但我确实有代码更改根视图从不支持导航堆栈的一个到一个.诀窍是我不在 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,它可能对你有用。

          【讨论】:

          • 这是一个有趣的想法,但考虑到相对简单的目标,这似乎是一种非常激进的方法。特别是如果有问题的导航堆栈只是 TabView 中的一个选项卡。我真的希望 Apple 能在不久的将来推出更多对 SwiftUI 的导航支持。
          • 哦,是的,这绝对是一个黑客,我也不喜欢每个人都必须得到SceneDelegate。如果您需要“立即”解决方案,它有效
          • 我做了类似的事情:stackoverflow.com/questions/57711277/…
          【解决方案17】:

          我想出了另一种可行的技术,但仍然感觉很奇怪。它仍然会为两个屏幕关闭动画,但它是一个 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 无法在不提供对其自身状态的访问权限的情况下创建任何一个目标视图,这意味着作者必须考虑提供所述访问权限的含义。

          【讨论】:

            【解决方案18】:

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

            首先,设置您的 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
                )
            }
            

            【讨论】:

              【解决方案19】:

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

              a gif shows how it works

              ContentView.swift 文件中:

              1. 定义一个RootSelection类,声明RootSelection@EnvironmentObject,只在根视图中记录当前活动NavigationLink的tag。
              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())
                      }
                  }
              }
              

              【讨论】:

                【解决方案20】:

                初级。 在根视图(您想要返回的位置)中使用 NavigationLink 和 isActive 设计器就足够了。在最后一个视图中,切换到控制 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"))
                    }
                }
                

                【讨论】:

                  【解决方案21】:

                  更容易呈现和关闭包含 NavigationView 的模态视图控制器。将模态视图控制器设置为全屏并稍后将其关闭会产生与弹出到根目录的导航视图堆栈相同的效果。

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

                  【讨论】:

                    猜你喜欢
                    • 2022-06-23
                    • 1970-01-01
                    • 2022-10-15
                    • 1970-01-01
                    • 1970-01-01
                    • 2020-06-26
                    • 1970-01-01
                    相关资源
                    最近更新 更多