Pop to root view using Tab Bar in SwiftUI
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")
}
}
How can I pop to the Root view using SwiftUI?
Setting the view modifier isDetailLink
to false
on a NavigationLink
is the key to getting pop-to-root to work. isDetailLink
is true
by default and is adaptive to the containing View. On iPad landscape for example, a Split view is separated and isDetailLink
ensures the destination view will be shown on the right-hand side. Setting isDetailLink
to false
consequently means that the destination view will always be pushed onto the navigation stack; thus can always be popped off.
Along with setting isDetailLink
to false
on NavigationLink
, pass the isActive
binding to each subsequent destination view. At last when you want to pop to the root view, set the value to false
and it will automatically pop everything off:
import SwiftUI
struct ContentView: View {
@State var isActive : Bool = false
var body: some View {
NavigationView {
NavigationLink(
destination: ContentView2(rootIsActive: self.$isActive),
isActive: self.$isActive
) {
Text("Hello, World!")
}
.isDetailLink(false)
.navigationBarTitle("Root")
}
}
}
struct ContentView2: View {
@Binding var rootIsActive : Bool
var body: some View {
NavigationLink(destination: ContentView3(shouldPopToRootView: self.$rootIsActive)) {
Text("Hello, World #2!")
}
.isDetailLink(false)
.navigationBarTitle("Two")
}
}
struct ContentView3: View {
@Binding var shouldPopToRootView : Bool
var body: some View {
VStack {
Text("Hello, World #3!")
Button (action: { self.shouldPopToRootView = false } ){
Text("Pop to root")
}
}.navigationBarTitle("Three")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Unwind NavigationView to root when switching tabs in SwiftUI
You'll need to keep track of the tab selection in the parent view and then pass that into the child views so that they can watch for changes. Upon seeing a change in the selection, the child view can then reset a @State
variable that change the isActive
property of the NavigationLink
.
class NavigationManager : ObservableObject {
@Published var activeTab = 0
}
struct MyTabView: View {
@StateObject private var navigationManager = NavigationManager()
var body: some View {
TabView(selection: $navigationManager.activeTab) {
TabOne().tabItem { Image(systemName: "1.square") }.tag(0)
TabTwo().tabItem { Image(systemName: "2.square") }.tag(1)
}.environmentObject(navigationManager)
}
}
struct TabOne: View {
var body: some View {
Text("1")
}
}
struct TabTwo: View {
@EnvironmentObject private var navigationManager : NavigationManager
@State private var linkActive = false
var body: some View {
NavigationView {
NavigationLink("Go to sub view", isActive: $linkActive) {
TabTwoSub()
}
}.onChange(of: navigationManager.activeTab) { newValue in
linkActive = false
}
}
}
struct TabTwoSub: View {
var body: some View {
Text("Tapping \(Image(systemName: "1.square")) doesnt unwind this view back to the root of the NavigationView")
.multilineTextAlignment(.center)
}
}
Note: this will result in a "Unbalanced calls to begin/end appearance transitions" message in the console -- in my experience, this is not an error and not something we have to worry about
SwiftUI ColorPicker cause pop to root view
NavigationView
in each tab instead of outside the TabView
How to dismiss a presenting view to the root view of tab view in SwiftUI?
The presentationMode
is one-level effect value, ie changing it you close one currently presented screen.
Thus to close many presented screens you have to implement this programmatically, like in demo below.
The possible approach is to use custom EnvironmentKey
to pass it down view hierarchy w/o tight coupling of every level view (like with binding) and inject/call only at that level where needed.
Demo tested with Xcode 12.4 / iOS 14.4
struct ContentView: View {
var body: some View {
TabView {
Text("Tab1")
.tabItem { Image(systemName: "1.square") }
Tab2RootView()
.tabItem { Image(systemName: "2.square") }
}
}
}
struct Tab2RootView: View {
@State var toRoot = false
var body: some View {
NavigationView {
Tab2NoteView(level: 0)
.id(toRoot) // << reset to root !!
}
.environment(\.rewind, $toRoot) // << inject here !!
}
}
struct Tab2NoteView: View {
@Environment(\.rewind) var rewind
let level: Int
@State private var showFullScreen = false
var body: some View {
VStack {
Text(level == 0 ? "ROOT" : "Level \(level)")
NavigationLink("Go Next", destination: Tab2NoteView(level: level + 1))
Divider()
Button("Full Screen") { showFullScreen.toggle() }
.fullScreenCover(isPresented: $showFullScreen,
onDismiss: { rewind.wrappedValue.toggle() }) {
Tab2FullScreenView()
}
}
}
}
struct RewindKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var rewind: Binding<Bool> {
get { self[RewindKey.self] }
set { self[RewindKey.self] = newValue }
}
}
struct Tab2FullScreenView: View {
@Environment(\.presentationMode) var mode
var body: some View {
Button("Close") { mode.wrappedValue.dismiss() }
}
}
Related Topics
Swift: Function with Default Parameter Before Non-Default Parameter
Perform a Segue Programmatically
Avoid Consecutive "If Let" Declarations in Swift
Enumerateobjectsusingblock in Swift
Difference Between Packed VS Normal Data Type
How to Rounded the Corners When I Draw Rectangle Using Uibezierpath Points
Segue Not Getting Selected Row Number
How to Check If an Email Address Is Already in Use Firebase
How to Specify That a Generic Is a Value Type
Create a Timer Publisher Using Swift Combine
Button Border with Transparent Background in Swift
Combining Two Conditions in Nspredicate
Unsafemutablepointer<Uint8> to [Uint8] Without Memory Copy
Update Core Data Object Order - Not Working