Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TabView resets navigation stack when switching tabs

Tags:

swift

swiftui

I have a simple 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)
}

When I go to the detail view on tab 1, switch to tab 2 then switch back to tab 1 I would assume to go back to the detail view (a basic UX found everywhere in iOS). Instead it resets to the root view of tab 1.

Since SwiftUI doesn't look to support this out of the box, how do I work around this?

like image 720
Casper Zandbergen Avatar asked Sep 03 '19 12:09

Casper Zandbergen


3 Answers

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")
                }
            }
        }
    }
}
like image 86
Casper Zandbergen Avatar answered Nov 16 '22 03:11

Casper Zandbergen


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)
        }
    }
}
like image 6
Russian Avatar answered Nov 16 '22 03:11

Russian


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 }))
    }
}
like image 2
John M. Avatar answered Nov 16 '22 02:11

John M.