Using SwiftUI Environments Values to change Tabs programmatically.

Ratnesh Jain

Feb 19, 2021 | 3 min read

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.

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.

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
                }
...

And here is the 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.