Reputation: 2688
Is there any way to pop to root view by tapping the Tab Bar like most iOS apps, in SwiftUI?
Here's an example of the expected behavior.
I've tried to programmatically pop views using simultaneousGesture
as follow:
import SwiftUI
struct TabbedView: View {
@State var selection = 0
@Environment(\.presentationMode) var presentationMode
var body: some View {
TabView(selection: $selection) {
RootView()
.tabItem {
Image(systemName: "house")
.simultaneousGesture(
TapGesture().onEnded {
self.presentationMode.wrappedValue.dismiss()
print("View popped")
}
)
}.tag(0)
Text("")
.tabItem {
Image(systemName: "line.horizontal.3")
}.tag(1)
}
}
}
struct RootView: View {
var body: some View {
NavigationView {
NavigationLink(destination: SecondView()) {
Text("Go to second view")
}
}
}
}
struct SecondView: View {
var body: some View {
Text("Tapping the house icon should pop back to root view")
}
}
But seems like those gestures were ignored.
Any suggestions or solutions are greatly appreciated
Upvotes: 25
Views: 8874
Reputation: 10346
You could do this with the help of UIKit by finding currently visible navigation controller for example as in the code below and calling popToRootViewController on it when user taps already selected tab. This works on iOS 15+:
struct TabbedView: View {
@State private var selection = 0
private var selectionBinding: Binding<Int> {
Binding(get: {
selection
}, set: {
if $0 == selection {
let window = (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.windows.first
let navigationController = window?.rootViewController?.recursiveChildren().first(where: { $0 is UINavigationController && $0.view.window != nil }) as? UINavigationController
navigationController?.popToRootViewController(animated: true)
}
selection = $0
})
}
var body: some View {
TabView(selection: selectionBinding) {
RootView(name: "first")
.tabItem {
Image(systemName: "house")
}.tag(0)
RootView(name: "second")
.tabItem {
Image(systemName: "table")
}.tag(1)
}
}
}
struct RootView: View {
var name: String
var body: some View {
NavigationView {
NavigationLink(destination: SecondView(name: name)) {
Text("Go to second view (\(name))")
}
}
}
}
struct SecondView: View {
var name: String
var body: some View {
Text("Tap current tab to pop back to root view (\(name))")
}
}
extension UIViewController {
func recursiveChildren() -> [UIViewController] {
return children + children.flatMap({ $0.recursiveChildren() })
}
}
Upvotes: 5
Reputation: 1
I would add animation to @StarRayder solution. It will simulate the default-like NavigationView's animation.
extension AnyTransition {
static var customTransition: AnyTransition {
let insertion = AnyTransition.move(edge: .leading)
let removal = AnyTransition.move(edge: .trailing)
return .asymmetric(insertion: insertion, removal: removal)
}
}
.transition(.customTransition)
Upvotes: 0
Reputation: 1297
In my own app I use the approach of changing the .id modifier on the topmost view which causes the NavigationView to pop to root. For clarity I also factored out this nav-popping behavior into its own wrapper view, "TabbedNavView", as shown below.
import SwiftUI
class TabMonitor: ObservableObject {
@Published var selectedTab = 1
}
struct ContentView: View {
@StateObject private var tabMonitor = TabMonitor()
var body: some View {
TabView(selection: $tabMonitor.selectedTab) {
TabbedNavView(tag: 1) {
DetailView(index: 1)
}
.tabItem { Label("Tab1", systemImage: "book") }
.tag(1)
TabbedNavView(tag: 2) {
DetailView(index: 10)
}
.tabItem { Label("Tab2", systemImage: "wrench") }
.tag(2)
} //TabView
.environmentObject(tabMonitor)
} //body
} //ContentView
struct DetailView: View {
var index: Int
var body: some View {
NavigationLink(
destination: DetailView(index: index + 1)
) {
Text("Detail \(index)")
}
} //body
} //DetailView
struct TabbedNavView: View {
@EnvironmentObject var tabMonitor: TabMonitor
private var tag: Int
private var content: AnyView
init(
tag: Int,
@ViewBuilder _ content: () -> any View
) {
self.tag = tag
self.content = AnyView(content())
} //init(tag:content:)
@State private var id = 1
@State private var selected = false
var body: some View {
NavigationView {
content
.id(id)
.onReceive(tabMonitor.$selectedTab) { selection in
if selection != tag {
selected = false
} else {
if selected {
id *= -1 //id change causes pop to root
}
selected = true
}
} //.onReceive
} //NavigationView
.navigationViewStyle(.stack)
} //body
} //TabbedNavView
The nice thing about this approach is that it works with any number of tabs, and any number of NavigationLinks within each tab. Each TabbedNavView keeps track of whether itself is the selected tab, and pops the NavigationView to root (by flipping the sign of the root view's .id modifier) whenever a tab is tapped twice in a row.
Edit: works with iOS 15+.
Upvotes: 3
Reputation: 21
This is a very simple solution that works for me.
I use a a state variable goHome
and assign a UUID()
to it.
Using onTapGesture
with a count of 2 assigns a new UUID()
to goHome
which resets the TabView
to its initial state.
Double tapping anywhere on any screen in the 'stack' resets tabview and takes you back to the tabview 'root'.
import SwiftUI
struct MainView: View {
@State private var tabSelection: Int = 1
@State private var goHome = UUID()
var body: some View {
DocumentsView()
.tabItem {
Image(systemName: "doc.plaintext.fill")
Text("Documents")
}.tag(3)
ToolsView()
.tabItem {
Image(systemName: "wrench.and.screwdriver.fill")
Text("Tools")
}.tag(4)
}
.id(goHome)
.onTapGesture(count: 2, perform: {
goHome = UUID()
})
}
}
Upvotes: 2
Reputation: 134
I messed around with this for a while and this works great. I combined answers from all over and added some stuff of my own. I'm a beginner at Swift so feel free to make improvements.
Here's a demo.
This view has the NavigationView.
import SwiftUI
struct AuthenticatedView: View {
@StateObject var tabState = TabState()
var body: some View {
TabView(selection: $tabState.selectedTab) {
NavigationView {
NavigationLink(destination: TestView(titleNum: 0), isActive: $tabState.showTabRoots[0]) {
Text("GOTO TestView #1")
.padding()
.foregroundColor(Color.white)
.frame(height:50)
.background(Color.purple)
.cornerRadius(8)
}
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
}
.navigationViewStyle(.stack)
.onAppear(perform: {
tabState.lastSelectedTab = TabState.Tab.first
}).tabItem {
Label("First", systemImage: "list.dash")
}.tag(TabState.Tab.first)
NavigationView {
NavigationLink(destination: TestView(titleNum: 0), isActive: $tabState.showTabRoots[1]) {
Text("GOTO TestView #2")
.padding()
.foregroundColor(Color.white)
.frame(height:50)
.background(Color.purple)
.cornerRadius(8)
}.navigationTitle("")
.navigationBarTitleDisplayMode(.inline).navigationBarTitle(Text(""), displayMode: .inline)
}
.navigationViewStyle(.stack)
.onAppear(perform: {
tabState.lastSelectedTab = TabState.Tab.second
}).tabItem {
Label("Second", systemImage: "square.and.pencil")
}.tag(TabState.Tab.second)
}
.onReceive(tabState.$selectedTab) { selection in
if selection == tabState.lastSelectedTab {
tabState.showTabRoots[selection.rawValue] = false
}
}
}
}
struct AuthenticatedView_Previews: PreviewProvider {
static var previews: some View {
AuthenticatedView()
}
}
class TabState: ObservableObject {
enum Tab: Int, CaseIterable {
case first = 0
case second = 1
}
@Published var selectedTab: Tab = .first
@Published var lastSelectedTab: Tab = .first
@Published var showTabRoots = Tab.allCases.map { _ in
false
}
}
This is my child view
import SwiftUI
struct TestView: View {
let titleNum: Int
let title: String
init(titleNum: Int) {
self.titleNum = titleNum
self.title = "TestView #\(titleNum)"
}
var body: some View {
VStack {
Text(title)
NavigationLink(destination: TestView(titleNum: titleNum + 1)) {
Text("Goto View #\(titleNum + 1)")
.padding()
.foregroundColor(Color.white)
.frame(height:50)
.background(Color.purple)
.cornerRadius(8)
}
NavigationLink(destination: TestView(titleNum: titleNum + 100)) {
Text("Goto View #\(titleNum + 100)")
.padding()
.foregroundColor(Color.white)
.frame(height:50)
.background(Color.purple)
.cornerRadius(8)
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView(titleNum: 0)
}
}
Upvotes: 5
Reputation: 433
We can use tab bar selection binding to get the selected index. On this binding we can check if the tab is already selected then pop to root for navigation on selection.
struct ContentView: View {
@State var showingDetail = false
@State var selectedIndex:Int = 0
var selectionBinding: Binding<Int> { Binding(
get: {
self.selectedIndex
},
set: {
if $0 == self.selectedIndex && $0 == 0 && showingDetail {
print("Pop to root view for first tab!!")
showingDetail = false
}
self.selectedIndex = $0
}
)}
var body: some View {
TabView(selection:selectionBinding) {
NavigationView {
VStack {
Text("First View")
NavigationLink(destination: DetailView(), isActive: $showingDetail) {
Text("Go to detail")
}
}
}
.tabItem { Text("First") }.tag(0)
Text("Second View")
.tabItem { Text("Second") }.tag(1)
}
}
}
struct DetailView: View {
var body: some View {
Text("Detail")
}
}
Upvotes: 10
Reputation: 516
You can achieve this by having the TabView within a NavigationView like so:
struct ContentView: View {
@State var selection = 0
var body: some View {
NavigationView {
TabView(selection: $selection) {
FirstTabView()
.tabItem {
Label("Home", systemImage: "house")
}
.tag(0)
}
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct FirstTabView: View {
var body: some View {
NavigationLink("SecondView Link", destination: SecondView())
}
}
struct SecondView: View {
var body: some View {
Text("Second View")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
}
}
Upvotes: -1