Introduction
Switching to a specific tab programmatically is done differently depending on the framework. In UIKit, set selectedIndex or selectedViewController on UITabBarController. In SwiftUI, bind a @State variable to TabView(selection:) and change that variable. In Android with Material Tabs, call TabLayout.getTabAt(index).select(). This is needed when deep links, push notifications, or user actions in one tab should navigate to another tab.
UIKit (Swift)
Using selectedIndex
1class MyViewController: UIViewController {
2
3 func switchToSecondTab() {
4 // selectedIndex is 0-based
5 self.tabBarController?.selectedIndex = 1
6 }
7}
Using selectedViewController
1func switchToProfileTab() {
2 guard let tabBarController = self.tabBarController else { return }
3
4 // Find the navigation controller containing ProfileViewController
5 for (index, vc) in tabBarController.viewControllers!.enumerated() {
6 if let nav = vc as? UINavigationController,
7 nav.viewControllers.first is ProfileViewController {
8 tabBarController.selectedIndex = index
9 break
10 }
11 }
12}
From AppDelegate (Deep Links)
1func application(_ app: UIApplication, open url: URL,
2 options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
3
4 guard let tabBarController = window?.rootViewController as? UITabBarController else {
5 return false
6 }
7
8 if url.host == "profile" {
9 tabBarController.selectedIndex = 2
10 } else if url.host == "settings" {
11 tabBarController.selectedIndex = 3
12 }
13
14 return true
15}
Handling UITabBarControllerDelegate
Control whether a tab switch should be allowed:
1class MainTabBarController: UITabBarController, UITabBarControllerDelegate {
2
3 override func viewDidLoad() {
4 super.viewDidLoad()
5 self.delegate = self
6 }
7
8 func tabBarController(_ tabBarController: UITabBarController,
9 shouldSelect viewController: UIViewController) -> Bool {
10 // Prevent switching to a tab that requires authentication
11 if viewController is ProfileViewController && !UserSession.isLoggedIn {
12 presentLoginScreen()
13 return false
14 }
15 return true
16 }
17}
SwiftUI
TabView with Selection Binding
1struct ContentView: View {
2 @State private var selectedTab = 0
3
4 var body: some View {
5 TabView(selection: $selectedTab) {
6 HomeView()
7 .tabItem {
8 Label("Home", systemImage: "house")
9 }
10 .tag(0)
11
12 SearchView()
13 .tabItem {
14 Label("Search", systemImage: "magnifyingglass")
15 }
16 .tag(1)
17
18 ProfileView(switchToHome: { selectedTab = 0 })
19 .tabItem {
20 Label("Profile", systemImage: "person")
21 }
22 .tag(2)
23 }
24 }
25}
Switching from a Child View
Pass the binding or use a closure:
1struct ProfileView: View {
2 var switchToHome: () -> Void
3
4 var body: some View {
5 VStack {
6 Text("Profile")
7 Button("Go to Home") {
8 switchToHome()
9 }
10 }
11 }
12}
1struct ContentView: View {
2 @State private var selectedTab = "home"
3
4 var body: some View {
5 TabView(selection: $selectedTab) {
6 HomeView()
7 .tabItem { Label("Home", systemImage: "house") }
8 .tag("home")
9
10 SettingsView()
11 .tabItem { Label("Settings", systemImage: "gear") }
12 .tag("settings")
13 }
14 .onOpenURL { url in
15 if url.host == "settings" {
16 selectedTab = "settings"
17 }
18 }
19 }
20}
Android (Kotlin)
1class MainActivity : AppCompatActivity() {
2
3 private lateinit var tabLayout: TabLayout
4 private lateinit var viewPager: ViewPager2
5
6 override fun onCreate(savedInstanceState: Bundle?) {
7 super.onCreate(savedInstanceState)
8 setContentView(R.layout.activity_main)
9
10 tabLayout = findViewById(R.id.tabLayout)
11 viewPager = findViewById(R.id.viewPager)
12
13 // Switch to tab at index 2
14 switchToTab(2)
15 }
16
17 fun switchToTab(index: Int) {
18 tabLayout.getTabAt(index)?.select()
19 viewPager.currentItem = index
20 }
21}
Bottom Navigation (Jetpack)
1class MainActivity : AppCompatActivity() {
2
3 private lateinit var bottomNav: BottomNavigationView
4
5 override fun onCreate(savedInstanceState: Bundle?) {
6 super.onCreate(savedInstanceState)
7 setContentView(R.layout.activity_main)
8
9 bottomNav = findViewById(R.id.bottom_navigation)
10
11 // Switch programmatically
12 bottomNav.selectedItemId = R.id.nav_profile
13 }
14}
Jetpack Compose
1@Composable
2fun MainScreen() {
3 var selectedTab by remember { mutableIntStateOf(0) }
4 val tabs = listOf("Home", "Search", "Profile")
5
6 Scaffold(
7 bottomBar = {
8 NavigationBar {
9 tabs.forEachIndexed { index, title ->
10 NavigationBarItem(
11 selected = selectedTab == index,
12 onClick = { selectedTab = index },
13 label = { Text(title) },
14 icon = { /* icon */ }
15 )
16 }
17 }
18 }
19 ) { padding ->
20 when (selectedTab) {
21 0 -> HomeScreen(onNavigateToProfile = { selectedTab = 2 })
22 1 -> SearchScreen()
23 2 -> ProfileScreen()
24 }
25 }
26}
React Native
1import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
2import { useNavigation } from '@react-navigation/native';
3
4const Tab = createBottomTabNavigator();
5
6function HomeScreen() {
7 const navigation = useNavigation();
8
9 return (
10 <Button
11 title="Go to Profile"
12 onPress={() => navigation.navigate('Profile')}
13 />
14 );
15}
16
17function App() {
18 return (
19 <Tab.Navigator>
20 <Tab.Screen name="Home" component={HomeScreen} />
21 <Tab.Screen name="Search" component={SearchScreen} />
22 <Tab.Screen name="Profile" component={ProfileScreen} />
23 </Tab.Navigator>
24 );
25}
Common Pitfalls
Setting selectedIndex before tabs are loaded: In UIKit, setting selectedIndex in viewDidLoad of the tab bar controller before all child view controllers are added has no effect. Set it in viewDidAppear or after adding all tabs.
Forgetting .tag() in SwiftUI TabView: Without .tag() on each tab, the selection binding cannot match tabs. Tags must be the same type as the @State variable (e.g., all Int or all String).
Not syncing TabLayout with ViewPager2 on Android: Calling tabLayout.getTabAt(index)?.select() alone does not scroll the ViewPager. Also set viewPager.currentItem = index to keep both in sync.
Animating tab switches when not desired: UIKit animates the tab switch by default with selectedViewController. To switch without animation, use selectedIndex directly. In SwiftUI, wrap the assignment in withAnimation(nil) to suppress animation.
Switching tabs from a background thread: UI updates must happen on the main thread. In UIKit, wrap in DispatchQueue.main.async { }. In SwiftUI, ensure @State changes happen on the @MainActor.
Summary
In UIKit, set tabBarController.selectedIndex or selectedViewController to switch tabs
In SwiftUI, bind a @State variable to TabView(selection:) and update the variable
In Android, use tabLayout.getTabAt(index)?.select() with viewPager.currentItem = index
In React Native, call navigation.navigate('TabName') from any screen
Always ensure tab tags/indices are set correctly and UI updates happen on the main thread