SwiftUI:如何弹出到根视图

时间:2019-08-02 23:37:12

标签: swift swiftui

最后,现在有了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()
        }
    }
}

19 个答案:

答案 0 :(得分:98)

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

isDetailLink上将false设置为NavigationLink的同时,将isActive绑定传递到每个后续目标视图。最后,当您要弹出到根视图时,将该值设置为false,它将自动弹出所有内容:

import SwiftUI

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

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

struct ContentView2: View {
    @Binding var rootIsActive : Bool

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

struct ContentView3: View {
    @Binding var shouldPopToRootView : Bool

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

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

Screen capture

答案 1 :(得分:10)

当然,@ malhal是解决方案的关键,但是对我来说,将Binding的参数作为参数传递给View的方法是不切实际的。正如@Imthath所指出的那样,环境是一种更好的方法。

这是在苹果公司发布的dismiss()方法之后弹出到以前的View的另一种方法。

定义对环境的扩展:

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添加到应从其弹出到根目录的所有子视图中。

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

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

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

答案 2 :(得分:8)

女士们,先生们,介绍苹果公司针对此问题的解决方案。 *还通过HackingWithSwift(我从大声笑中偷走了)介绍给您:under programmatic navigation

(在Xcode 12和iOS 14上测试)

基本上,您在tag内使用selectionnavigationlink直接进入所需的任何页面。

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

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

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

注入到应用程序中

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

并使用它:

struct ContentView: View {
@EnvironmentObject var navigationHelper: NavigationHelper

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

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

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

答案 3 :(得分:7)

我花了最后几个小时来尝试解决相同的问题。据我所知,使用当前的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中不提供导航堆栈的管理。

NB:以下通知提供的简单解决方案适用于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)
    }
}

答案 4 :(得分:7)

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

答案 5 :(得分:4)

对我来说,为了完全控制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

答案 6 :(得分:2)

花了一些时间,但我弄清楚了如何在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
}

通过两个标签设置标签视图,然后将NavigationController.selection绑定到标签视图:

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状态导向的。

答案 7 :(得分:1)

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

答案 8 :(得分:1)

我最近创建了一个名为swiftui-navigation-stackhttps://github.com/biobeats/swiftui-navigation-stack)的开源项目。它是SwiftUI的替代导航堆栈。看一下自述文件中的所有细节,它真的很容易使用。

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

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

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

然后将根目录嵌入NavigationStackView中(就像使用标准NavigationView一样):

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

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

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

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

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

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

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

结果是:

enter image description here

答案 9 :(得分:1)

感谢您的@Binding解决方案“ Malhal”。我缺少.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()
        }
    }

第3步: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()
    }
}

enter image description here

答案 10 :(得分:1)

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

答案 11 :(得分:1)

这是我的解决方案,适用于任何地方,无需依赖。

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)

答案 12 :(得分:0)

初级。 在根视图(您想要返回的位置)中使用带有 isActive 设计器的 NavigationLink 就足够了。在最后一个视图中,切换到控制 isActive 参数的 FALSE 变量。

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

您可以使用我在示例中使用的一些通用类,或者通过绑定将这个变量向下传递到 VIEW 层次结构。使用它对您更方便的方式。

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

@main
struct TestPopToRootApp: App {
    let vm = ViewModel()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(vm)
        }
    }
}

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

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

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

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

答案 13 :(得分:0)

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

a gif shows how it works

ContentView.swift 文件中:

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

ContentView()

xxxApp.swift

答案 14 :(得分:0)

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

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

答案 15 :(得分:0)

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

答案 16 :(得分:0)

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

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

答案 17 :(得分:0)

我没有完全相同的问题,但是我有一些代码将根视图从不支持导航堆栈的代码更改为不支持导航堆栈的代码。诀窍是我没有在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,那么它可能对您有用。

答案 18 :(得分:-1)

我想出了另一种有效的技术,但仍然感觉很奇怪。它还可以对两个屏幕进行动画处理,但这是一个清洁器。您可以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在不提供进入其自身状态的访问权限的情况下无法创建任何一个目的地视图,这意味着作者必须仔细考虑提供所述访问权限的含义。