In this post, we will explore the Environment Key
and EnvironmentValues
to achieve the programmatic tab switching in TabView
.
Lets start with the empty SwiftUI template from the Xcode and Create new SwiftUI view named AppTabView.swift.
Define AppTabView
Here we define our tabs in AppTabView like below.
struct AppTabView: View {
enum Tab {
case profile
case bookmarks
case settings
}
@State private var selectedTab: Tab = .profile
var body: some View {
TabView(selection: $selectedTab) {
ProfileView()
.tabItem {
Image(systemName: "person")
Text("Profile")
}
.tag(Tab.profile)
BookmarksView()
.tabItem {
Image(systemName: "book")
Text("Bookmarks")
}
.tag(Tab.bookmarks)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}
.tag(Tab.settings)
}
}
}
Here you can see we have used the .tag
modifier in order to TabView to switch the current tab.
If we use @SceneStorage("selectedTab") instead of @State, then we can get some level of state-restoration behaviour.
Now below is the contents of the ProfileView
, BookmarksView
and SettingsView
.
struct ProfileView: View {
var body: some View {
NavigationView {
List {
Button("Bookmarks") {
print("Switch to Bookmarks Tab")
}
Button("Settings") {
print("Switch to Settings Tab")
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Profile")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct BookmarksView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: BookmarkDetailView()) {
Text("ICE")
}
Button("Settings") {
print("Switch to Settings Tab")
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Bookmarks")
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct SettingsView: View {
var body: some View {
NavigationView {
List {
Button("Profile") {
print("Switch to Profile View")
}
Button("Bookmarks") {
print("Switch to Bookmarsk View")
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
}
}
}
Now we have the our basic tabs setup, now we need some way to pass the button press action to super view ( AppTabView
in our case ). So we can use the EnvironmentKey
and EnvironmentValues
here.
So let's start defining our EnviromentKey named CurrentTabKey
like below.
struct CurrentTabKey: EnvironmentKey {
static var defaultValue: AppTabView.Tab = .bookmarks
}
To use above EnvironmentKey
we need to extend the EnvironmentValues
like below.
extension EnvironmentValues {
var currentTab: AppTabView.Tab {
get { self[CurrentTabKey.self] }
set { self[CurrentTabKey.self] = newValue }
}
}
Now we can use this CurrentTabKey
like below.
...
TabView(selection: $selectedTab) {
ProfileView()
.tabItem {
Image(systemName: "person")
Text("Profile")
}
.tag(Tab.profile)
.environment(\.currentTab, selectedTab)
...
And in the ProfileView
we can use the environment property wrapper like below.
struct ProfileView: View {
@Environment(\.currentTab) var tab
var body: some View {
NavigationView {
List {
Button("Bookmarks") {
tab = .bookmarks
}
Button("Settings") {
tab = .settings
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Profile")
.navigationBarTitleDisplayMode(.inline)
}
}
}
Here we can use the environment variable in our button action. But...
Cannot assign to property: 'tab' is a get-only property
To solve this, we need to recall the two way data communication in SwiftUI. For this SwiftUI has property wrapper called Binding
.
Set CurrentTabKey
So we update our EnvironmentKey struct CurrentTabKey like below.
struct CurrentTabKey: EnvironmentKey {
static var defaultValue: Binding<AppTabView.Tab> = .constant(.bookmarks)
}
And EnvironmentValues like below.
extension EnvironmentValues {
var currentTab: Binding<AppTabView.Tab> {
get { self[CurrentTabKey.self] }
set { self[CurrentTabKey.self] = newValue }
}
}
Cannot convert value 'selectedTab' of type 'AppTabView.Tab' to expected type 'Binding<AppTabView.Tab>', use wrapper instead
Insert $
Fixing the above compiler error by putting the $
in the environment modifier like below.
...
ProfileView()
.tabItem {
Image(systemName: "person")
Text("Profile")
}
.tag(Tab.profile)
.environment(\.currentTab, $selectedTab)
...
And now finaly updating our Views like below.
...
Button("Bookmarks") {
tab.wrappedValue = .bookmarks
}
Button("Settings") {
tab.wrappedValue = .settings
}
...
Set BookmarkDetailView
struct BookmarkDetailView: View {
@Environment(\.currentTab) var tab
var body: some View {
List {
Button("Profile") {
tab.wrappedValue = .profile
}
}
.listStyle(InsetGroupedListStyle())
.navigationTitle("Bookmark Detail")
.navigationBarTitleDisplayMode(.inline)
}
}
And here we have it.
We hope you liked it. Thanks for reading.