Pop to root view using Tab Bar in SwiftUI

Timo
5 min readMay 24, 2023

--

Navigate from SubView to RootView by tapping on a TabBar item

In the past, going back to the root view in SwiftUI was a bit cumbersome and required some compromises. However, with the introduction of the NavigationStack in iOS 16, this process has become much simpler. Now, users can effortlessly return to the RootView by utilizing the TabBar.

In this narrative, we will explore the straightforward method of navigating to the RootView by making use of the NavigationStack and NavigationPath features.

The Tabbar

Let’s focus on the fundamental component known as the TabBar. As the name suggests, it serves as the foundation for our navigation. A basic TabBar with just one tab will suffice for our purposes (although you have the flexibility to expand and customize it as needed).

TabView() {
// RootView
Text("Hello World")
.tabItem {
Image(systemName: "square.and.pencil")
}
}

To enable the TabView to determine the currently selected tab, we must introduce a variable that stores and updates our selection. This variable acts as a cache, holding the value of the currently selected tab. By establishing this mechanism, the TabView becomes aware of the active tab at any given time. This enables us to track and manipulate the selected tab dynamically as per our requirements.

@State private var selection = 0

To create a connection between the variable and the TabView, we use the concept of binding variables. This involves passing the variable as a binding parameter to the TabView, thereby creating a two-way link between them. By doing so, any changes made to the variable will automatically reflect in the TabView, and vice versa. This binding mechanism ensures that the TabView and the variable stay in sync. Consequently, the TabView can easily reflect the current state of the variable and respond accordingly to any updates or modifications.

TabView(selection: self.$selection) {
// RootView
Text("Hello World")
.tabItem {
Image(systemName: "square.and.pencil")
}
.tag(0)
}

It’s important to highlight the significance of the “.tag(0)” modifier in this context. This modifier tells the TabView to associate the view “Text(“Hello World”)” with the first tab. By assigning a tag value of 0 to this view, we establish a connection between the tab’s selection and the content it displays. Consequently, when the TabView’s selection matches the tag value of 0 (representing the first tab), the corresponding content, in this case, the “Hello World” text, will be presented. In essence, the “.tag(0)” modifier acts as a mapping mechanism, enabling the TabView to identify and display the appropriate content based on the selected tab.

NavigationStack

Starting from iOS 16, SwiftUI introduced the NavigationStack concept, which changes the way views are managed. With the NavigationStack, views can be stacked on top of each other and dynamically changed through code. This flexibility allows developers to easily remove views from the stack and enabling a smooth return to the RootView without using UIKit. By using the NavigationStack, SwiftUI provides a powerful mechanism for efficient view management and navigation, enhancing the overall user and developer experience.

TabView(selection: self.$selection) {
NavigationStack(root: {
// RootView
Text("Hello World")
})
.tabItem {
Image(systemName: "square.and.pencil")
}
.tag(0)
}

NavigationPath

A NavigationPath helps keep track of the current path as views are opened or closed. It dynamically updates to reflect the ongoing navigation within the app. By utilizing the NavigationPath, developers can easily monitor and manage the user’s progression through the app’s interface.

@State var path: [String] = []

Adding the NavigationPath into the NavigationStack is our next step. By integrating the NavigationPath, we ensure that the navigation flow and path are accurately captured within the stack. This integration allows us to effectively track and manage the user’s navigation, providing an overview of their navigation through the app’s interface.

TabView(selection: self.$selection) {
NavigationStack(path: self.$path, root: {
// Root View
Text("Hello World")
})
.tabItem {
Image(systemName: "square.and.pencil")
}
.tag(0)
}

When the NavigationPath is empty, it indicates that we are currently on the RootView. This implies that it is possible to navigate back to the RootView by simply emptying the NavigationPath. By recognizing this condition and using this, we can enable a seamless return to the RootView.

Selection handler

In order to empty the path, we need to respond to taps on the TabBar. To accomplish this, we utilize the selection variable and implement a handler that can react to any changes. By implementing this handler, we can ensure that appropriate actions are taken when the user interacts with the TabBar, enabling us to empty the NavigationPath as we want it.

var handler: Binding<Int> { Binding(
get: { self.selection },
// React to taps on the tap item
set: {
// If the current selection gets tapped again
if $0 == self.selection {
if $0 == 0 {
// Empty the navigation path
self.path.removeAll()
}
}
self.selection = $0
}
) }

Now, we use this handler to control the TabView’s behavior. By connecting the handler to the TabView, we can define how the TabView responds to user interactions. This enables us to integrate the desired functionality.

TabView(selection: self.handler) {
NavigationStack(path: self.$path, root: {
// RootView
Text("Hello World")
})
.tabItem {
Image(systemName: "square.and.pencil")
}
.tag(0)
}

Open a new View

To open a new view, SwiftUI provides the convenient NavigationLink. By utilizing a NavigationLink, we can easily trigger the presentation of a new view. This feature simplifies the process of navigating between views, allowing for smooth transitions and an enhanced user interface. In this case the “Text(“Subview”)” is not the actual view we will open, it is the label for the NavigationLink.

NavigationLink(value: /*An id or link*/) {
Text("SubView")
}

To open the new view, we can use the power of the “.navigationDestination” modifier in SwiftUI. By applying this modifier to the NavigationLink, we can define the destination view that should be presented when the link is activated.

Note that the String we pass in the NavigationLink can be used in the .navigationDestination modifier, to handle the opening of the new view.

View()
.navigationDestination(for: String.self) { id in
SubView(id: id)
}

The passed NavigationPath is automatically expanded.

Monitor NavigationPath

Within the TabView, we can observe and respond to changes in the NavigationPath by utilizing the “onChange” modifier. This modifier allows us to monitor and track the NavigationPath, enabling us to react accordingly whenever it undergoes modifications. By using the “onChange” modifier, we can easily observe and respond to changes in the NavigationPath.

TabView {
...
}
.onChange(of: self.path) { newValue in
print(newValue)
}

Summary

With the introduction of NavigationStack and NavigationPath features, managing views and tracking the navigation path becomes simpler. By following the provided steps, you can set up a TabBar, bind variables, handle selections, open new views using NavigationLink, and monitor changes.

Check out my socials:

--

--