TabView resets navigation stack when switching tabs
Solution 1:
The not so obvious solution here was to actually not use SwiftUI. To get the UIKit behaviour I wrapped a UIKit UITabBarController
in a SwiftUI UIViewControllerRepresentable
like in this example: https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit.
I show a basic implementation here. The full up to date implementation is on github: https://gist.github.com/Amzd/2eb5b941865e8c5cccf149e6e07c8810
Wrap the UIKit UITabBarController in a SwiftUI view:
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) {
}
}
Example usage:
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")
}
}
}
}
}
Solution 2:
Here's a simple example of how to preserve state for a navigation stack with a list of items at the root:
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)
}
}
}
Solution 3:
So, this does "preserve" the detail view when switching tabs, but only by visibly pushing the detail view when switching back to tab 1. I have been unsuccessful at disabling this with, for example, .animation()
.
In addition, you pretty much have to override the navigation bar items in the DetailView
, because the default back button behaves oddly (comment out the .navigationBarItems()
line to see what I mean).
With those caveats, this does qualify as a workaround.
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 }))
}
}