如何在SwiftUI中从TabView的子视图导航回TabView?

3
在SwiftUI中,TabView必须是根视图。因此,您不能使用NavigationLink导航到TabView。例如,我有四个屏幕的应用程序。
屏幕A是一个包含屏幕B和屏幕C的TabView。 屏幕B是一个列表,其中包含导航链接以进入列表项详情页面(屏幕D)。 屏幕C是一个信息视图(在此问题中不重要)。 屏幕D是一个列表项详情页面。您必须先导航到屏幕B才能进入该页面。但是,屏幕D有一个按钮,应该在ViewModel中执行网络操作,完成后将您带回Screen A。
如何使屏幕D导航返回两级到根屏幕(屏幕A)?

如果ScreenA是TabView {ScreenB | ScreenC},并且ScreenB> ScreenD,则我看不到两级导航,我错过了什么吗?情景不太清楚。 - Asperi
3个回答

3
“弹回”到根视图的一种有效方法是在用于导航的NavigationLink上使用isDetailLink修饰符。默认情况下,isDetailLinktrue。此修饰符用于各种视图容器,例如在iPad上,详细视图会显示在右侧。将isDetailLink设置为false意味着该视图将被推到NavigationView堆栈的顶部,并且也可以被推出。除了在NavigationLink上将isDetailLink设置为false外,还需要将isActive绑定传递给每个子目标视图。当您想要弹回到根视图时,将值设置为false,它将把所有内容都弹出:
import SwiftUI

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

    var body: some View {
        NavigationView {
            NavigationLink(
                destination: ScreenB(rootIsActive: self.$isActive),
                isActive: self.$isActive
            ) {
                Text("ScreenA")
            }
            .isDetailLink(false)
            .navigationBarTitle("Screen A")
        }
    }
}

struct ScreenB: View {
    @Binding var rootIsActive : Bool

    var body: some View {
        NavigationLink(destination: ScreenD(shouldPopToRootView: self.$rootIsActive)) {
            Text("Next screen")
        }
        .isDetailLink(false)
        .navigationBarTitle("Screen B")
    }
}

struct ScreenD: View {
    @Binding var shouldPopToRootView : Bool

    var body: some View {
        VStack {
            Text("Last Screen")
            Button (action: { self.shouldPopToRootView = false } ){
                Text("Pop to root")
            }
        }.navigationBarTitle("Screen D")
    }
}

但是,MVVM架构下该如何实现呢?在Swift中,每个屏幕都应该有一个ViewModel。因此,假设你已经到达D屏幕,你按下一个按钮,在ViewModel中执行网络调用。当网络调用完成并成功时,您应该弹出到根视图。 - Adrian Le Roy Devezin
好的,但是当屏幕B显示时,我希望TabView导航栏必须隐藏 - 如何做到? - Igor Cova
@IgorCova 如果要隐藏选项卡栏,您可以将TabView放置在NavigationView中。1.NavigationView->Link->TabView屏幕->每个选项卡都可以包含NavigationLink,而不是View。 - Oleksii Radetskyi

2

我曾经做过类似的技巧,对我很有效。

在SceneDelegate.swift文件中,我修改了自动生成的代码。


let contentView = ContentView()

if let windowScene = scene as? UIWindowScene {
   let window = UIWindow(windowScene: windowScene)
   // Trick here.
   let nav = UINavigationController(
       rootViewController: UIHostingController(rootView: contentView)
    )
    // I embedded this host controller inside UINavigationController
    //  window.rootViewController = UIHostingController(rootView: contentView)
    window.rootViewController = nav
    self.window = window
    window.makeKeyAndVisible()
}

注意: 我原本期望将TabView嵌入NavigationView中会达到同样的效果,但是并没有成功,这就是我使用这个技巧的原因。

我假设,contentView是你想要弹出的视图(包含TabView的那个视图)

然后在从那个视图导航到的任何视图中,你都可以调用

extension View {
    func popToRoot() {
        guard let rootNav = UIApplication.shared.windows.first?.rootViewController as? UINavigationController else { return }
        rootNav.popToRootViewController(animated: true)
    }
}

// Assume you eventually came to this view.
struct DummyDetailView: View {

    var body: some View {

        Text("DetailView")
           .navigationBarItems(trailing:
               Button("Pop to root view") {
                   self.popToRoot()
               }
           )
    }
}

// EDIT: Requested sample with a viewModel
struct DummyDetailViewWithViewModel: View {

    var viewModel: SomeViewModel = SomeViewModel()

    var body: some View {        
        Button("Complete Order!!") {
            viewModel.completeOrder(success: { _ in
                print("Order Completed")
                self.popToRoot()
            })
        }
    }
}

但是,这个MVVM设置如何工作呢?在Swift中,每个屏幕都应该有一个ViewModel。所以,假设你现在在D屏幕上,你按下一个按钮,在ViewModel中执行一个网络调用。然后,当该网络调用完成并成功时,您应该弹出到根视图。 - Adrian Le Roy Devezin
无论您在哪里调用它都没有关系。例如:您点击了一个按钮并进行了网络调用。 您可以向执行操作的视图返回成功回调,因此您可以在那里触发该方法。 - Enes Karaosman
你可以隐藏它,但如果你想推到详细视图,你必须在导航视图中,这就是它的工作原理。 - Enes Karaosman
如果你想要去掉导航栏,我建议使用这个解决方案 - Joshua Martinez

2

我已经通过self.presentationMode.wrappedValue.dismiss()解决了这个问题。这是调用的方法,用于返回导航根视图。在这里,TestView是ScreenA,ItemList是ScreenB,InfoView是ScreenC,ItemDetails是ScreenD。

import SwiftUI

struct TestView: View {
    @State private var currentTab: Tab = .list
    var body: some View {
        TabView(selection: $currentTab){
            ItemList()
                .tabItem{
                    Text("List")
            }
            .tag(Tab.list)
            .navigationBarHidden(false)
            InfoView()
                .tabItem{
                    Text("Info")
            }
            .tag(Tab.info)
            .navigationBarHidden(true)
        }
    }
}

struct ItemList: View {
    var body: some View {
        VStack{
            NavigationView{
                List {
                    NavigationLink(destination: ItemDetails()){
                        Text("Item")
                    }
                    NavigationLink(destination: ItemDetails()){
                        Text("Item")
                    }
                    NavigationLink(destination: ItemDetails()){
                        Text("Item")
                    }
                    NavigationLink(destination: ItemDetails()){
                        Text("Item")
                    }
                }.navigationBarTitle("Item List")
            }
        }
    }
}

struct InfoView: View {
    var body: some View {
        Text("This is information view")
    }
}

struct ItemDetails: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @State private var loading = false
    var body: some View {
        ZStack {
            Text("Connecting...")
                .font(.title)
                .offset(y: -150)
                .pulse(loading: self.loading)
            VStack {
                Text("This is Item Details")
                Button("Connect"){
                    self.loading = true
                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                        self.loading = false
                        self.presentationMode.wrappedValue.dismiss()
                    }
                }.padding()
            }
        }
    }
}

enum Tab {
    case list, info
}

extension View {
    func pulse(loading: Bool) -> some View {
        self
            .opacity(loading ? 1 : 0)
            .animation(
                Animation.easeInOut(duration: 0.5)
                    .repeatForever(autoreverses: true)
        )

    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接