【问题标题】:TabView resets navigation stack when switching tabsTabView 在切换选项卡时重置导航堆栈
【发布时间】:2020-01-06 09:18:27
【问题描述】:

我有一个简单的 TabView:

TabView {
    NavigationView {
        VStack {
            NavigationLink(destination: Text("Detail")) {
                Text("Go to detail")
            }
        }
    }
        .tabItem { Text("First") }
        .tag(0)
    Text("Second View")
        .tabItem { Text("Second") }
        .tag(1)
}

当我转到选项卡 1 上的详细视图时,切换到选项卡 2,然后切换回选项卡 1,我会假设返回到详细视图(iOS 中随处可见的基本 UX)。相反,它会重置为选项卡 1 的根视图。

由于 SwiftUI 不支持开箱即用,我该如何解决这个问题?

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    这里不那么明显的解决方案是实际上不使用 SwiftUI。为了获得 UIKit 行为,我将 UIKit UITabBarController 包装在 SwiftUI UIViewControllerRepresentable 中,如下例所示:https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit

    我在这里展示了一个基本的实现。完整的最新实现在 github 上:https://gist.github.com/Amzd/2eb5b941865e8c5cccf149e6e07c8810

    将 UIKit UITabBarController 包装在 SwiftUI 视图中:

    struct UIKitTabView: View {
        var viewControllers: [UIHostingController<AnyView>]
    
        init(_ tabs: [Tab]) {
            self.viewControllers = tabs.map {
                let host = UIHostingController(rootView: $0.view)
                host.tabBarItem = $0.barItem
                return host
            }
        }
    
        var body: some View {
            TabBarController(controllers: viewControllers)
                .edgesIgnoringSafeArea(.all)
        }
    
        struct Tab {
            var view: AnyView
            var barItem: UITabBarItem
    
            init<V: View>(view: V, barItem: UITabBarItem) {
                self.view = AnyView(view)
                self.barItem = barItem
            }
        }
    }
    
    struct TabBarController: UIViewControllerRepresentable {
        var controllers: [UIViewController]
    
        func makeUIViewController(context: Context) -> UITabBarController {
            let tabBarController = UITabBarController()
            tabBarController.viewControllers = controllers
            return tabBarController
        }
    
        func updateUIViewController(_ uiViewController: UITabBarController, context: Context) {
    
        }
    }
    

    示例用法:

    struct ExampleView: View {
        @State var text: String = ""
    
        var body: some View {
            UIKitTabView([
                UIKitTabView.Tab(
                    view: NavView(), 
                    barItem: UITabBarItem(title: "First", image: nil, selectedImage: nil)
                ),
                UIKitTabView.Tab(
                    view: Text("Second View"), 
                    barItem: UITabBarItem(title: "Second", image: nil, selectedImage: nil)
                )
            ])
        }
    }
    
    struct NavView: View {
        var body: some View {
            NavigationView {
                VStack {
                    NavigationLink(destination: Text("This page stays when you switch back and forth between tabs (as expected on iOS)")) {
                        Text("Go to detail")
                    }
                }
            }
        }
    }
    

    【讨论】:

    • 您好,这是一个非常好的解决方法,但导航栏仍然存在一些问题。我使用的是 NavigationView(就像您的第一个选项卡一样),而不是每个选项卡内容的简单文本视图。这适用于前 4 个选项卡。但是,对于“更多”下的选项卡,您会得到一个双导航栏。顶部导航栏显示“更多”,下方的另一个是带有我自己标题的 SwiftUI 导航栏。你知道这个问题吗?你知道解决方法吗?
    • @ThomasVos 我不知道。我不使用 UITabBarController 的“更多”功能,因为我认为它对用户非常不友好
    • @CasperZandbergen:谢谢!我正在使用您的解决方案,但我想添加在运行时添加新选项卡的功能。知道怎么做吗?只是创建一个新的 Tab 并不会添加一个新的 ViewController
    • @CasperZandbergen 这个解决方案比你标记的那个更好。 TabView 应自行保留选项卡状态(或至少可选)
    【解决方案2】:

    下面是一个简单示例,说明如何为导航堆栈保留状态,并在根目录中包含项目列表:

    struct ContentView: View {
    
        var body: some View {
    
            TabView {
    
                Text("First tab")
                    .tabItem { Image(systemName: "1.square.fill"); Text("First") }
                    .tag(0)
    
                SecondTabView()
                    .tabItem { Image(systemName: "2.square.fill"); Text("Second") }
                    .tag(1)
            }
        }
    }
    
    struct SecondTabView: View {
    
        private struct ListItem: Identifiable {
            var id = UUID()
            let title: String
        }
    
        private let items = (1...10).map { ListItem(title: "Item #\($0)") }
    
        @State var selectedItemIndex: Int? = nil
    
        var body: some View {
    
            NavigationView {
                List(self.items.indices) { index in
                    NavigationLink(destination:  Text(self.items[index].title),
                                   tag: index,
                                   selection: self.$selectedItemIndex) {
                        Text(self.items[index].title)
                    }
                }
                .navigationBarTitle("Second tab", displayMode: .inline)
            }
        }
    }
    

    【讨论】:

    • 这适用于我的 Xcode 11.3 测试版,所以今后这比我的解决方案要好。这在 Xcode 11.0(我安装的唯一用于测试它的旧版本)中不起作用
    • @Casper Zandbergen,感谢您的编辑建议。从我的代码改编时错过了它。
    • 而且这个解决方案实际上并不通用(这就是为什么我提出“带有项目列表的导航堆栈”评论),所以你的仍然是相关的。它仅适用于可以从某些持久数据重新创建 UI 状态的情况(在这种情况下为selectedItemIndex)。例如,如果每个选项卡中都有一个 Web 视图,那么在它们之间切换会导致它们重新加载。
    • 由于最初的问题纯粹是关于导航堆栈重置的问题,我仍然会将您的答案作为已接受的答案。
    • 这是正确的答案。如果“应用程序是状态的函数”,那么您需要在某个地方存储该状态,否则该状态将丢失。将事物绑定到 ViewModel 中的属性(例如 TabView 的选定选项卡或 NavigationLink 的选择)还为您提供了一种在应用重新启动时进行状态恢复的方法。
    【解决方案3】:

    因此,这确实在切换选项卡时“保留”了详细信息视图,但只能通过在切换回选项卡 1 时明显推送详细信息视图。我未能成功禁用此功能,例如,.animation()

    此外,您几乎必须覆盖DetailView 中的导航栏项目,因为默认的后退按钮的行为很奇怪(注释掉.navigationBarItems() 行以了解我的意思)。

    有了这些警告,这确实可以作为一种解决方法。

    struct ContentView: View {
        @State var showingDetail = false
    
        var body: some View {
            TabView {
                NavView(showingDetail: $showingDetail)
                    .tabItem { Text("First") }
                    .tag(0)
                Text("Second View")
                    .tabItem { Text("Second") }
                    .tag(1)
            }
        }
    }
    
    struct NavView: View {
        @Binding var showingDetail: Bool
    
        var body: some View {
            NavigationView {
                VStack {
                    NavigationLink(destination: DetailView(showing: $showingDetail), isActive: $showingDetail) {
                        Text("Go to detail")
                    }
                }
            }
        }
    }
    
    struct DetailView: View {
        @Binding var showing: Bool
    
        var body: some View {
                Text("Detail")
                    .navigationBarItems(leading: Button("Back", action: { self.showing = false }))
        }
    }
    

    【讨论】:

    • 有趣的解决方法,虽然这会破坏向后滑动并且实际上并没有保存页面,只是重新创建它。因此,如果您有一个包含内容的文本字段,它将被清除。我还认为,当您开始添加更多详细视图时,这会变得非常混乱。
    • 是的,没错。理论上,您可以传入一个@ObservedObject 模型来跟踪详细视图的状态,但是是的,很混乱。但是,如果您的详细视图主要是演示,它可能会很有用。此处指出损坏的默认后退按钮:swiftui-lab.com/bug-navigationlink-isactive。我希望看到持久的 NavigationView 状态,所以也许我们都只需要向 Apple 提交反馈。