<aside> 💡 SwiftUI 中的文件名不要起的跟常用的View的名字一样,例如一个文件名叫TabView的文件,里面又用了TabView(){ … }这个视图,会莫名其妙报错。

</aside>

TabView

NavigationStack 非常适合创建分层视图堆栈,让用户深入了解数据,但它们不太适合展示不相关的数据。想要展示一些不相关的视图,经常需要使用 TabView ,它会在屏幕底部创建一个按钮条,点击每个按钮会显示不同的视图。TabView 就像是其自身内部的子视图的容器。这些子视图是单独的屏幕,它提供标签按钮 TabItems ,允许用户在这些子视图之间切换;当标签太多,无法满足设备的需求时,就会创建 "更多 "按钮,在这里可以找到其余的标签。


针对 iOS 18 及更新版本

对于 iOS 18 及更高版本, TabView 是从多个单独的 Tab 视图创建的。

Tab 的设置

每个 Tab 视图都有一个标题和一个图标。如果这些选项卡之一负责搜索您的应用,请添加 .search 的 role 参数。

TabView {
		// 每个视图都有一个标题和一个图标
    Tab("Home", systemImage: "house") {
        Text("Put a HomeView here")
    }
    Tab("Users", systemImage: "person.3") {
        Text("Put a UsersView here")
    }
    Tab("Search", systemImage: "magnifyingglass", role: .search) {
        Text("Put a SearchView here")
    }
}

当想要在 iPadOS 侧边栏中将选项卡分组在一起时,事情会变得更有趣。选项卡组是通过在 TabSection 内放置一个或多个 Tab 视图来创建的,我们可以允许用户使用 sidebarAdaptable 风格。

TabView {
    TabSection("Watch") {
        Tab("Movies", systemImage: "film") {
            Text("Put a MoviesView here")
        }
        Tab("TV Shows", systemImage: "tv") {
            Text("Put a TVShowsView here")
        }
    }

    TabSection("Listen") {
        Tab("Music", systemImage: "music.note.list") {
            Text("Put a MusicView here")
        }

        Tab("Podcasts", systemImage: "mic") {
            Text("Put a PodcastsView here")
        }
    }
}
.tabViewStyle(.sidebarAdaptable)

具体的外观取决于用户使用的设备以及他们激活的选项卡视图模式。

以编程方式控制

如果想以编程方式控制选项卡选择,请绑定到 TabView 的 selection ,然后将适当的 value 参数添加到 Tab 对象。

struct ContentView: View {

		// 使用枚举是提供类型安全的好方法,Swift 知道选择必须是某种 Section ,因此它不允许不是 .cats 的值或 .dogs
    enum Section {
        case cats
        case dogs
    }

    @State private var selectedTab = Section.cats

    var body: some View {
        TabView(selection: $selectedTab) {
            Tab("Cats", systemImage: "cat", value: .cats) {
                Button("Go to Dogs") {
                    selectedTab = .dogs
                }
            }

            Tab("Dogs", systemImage: "dog", value: .dogs) {
                Button("Go to Cats") {
                    selectedTab = .cats
                }
            }
        }
    }
    
}

针对 iOS 17 及更早版本

在 iOS 17 及更早版本中,tabView 要为每个项目提供图像和标题,如果想以编程方式控制哪个选项卡处于活动状态,还可以选择添加标签。

// 基本创建方式
TabView {
		//第一个页面
    Text("Tab 1")
				//在底部标签栏创建一个标签按钮,指向第一个页面
        .tabItem {
            Label("One", systemImage: "star")
        }
		//第二个页面    
    Text("Tab 2")
				//在底部标签栏创建第二个标签按钮,指向第二个页面
        .tabItem {
            Label("Two", systemImage: "circle")
        }
}
// 如果把每个Tab视图拆分得比较好,一般这样写:
TabView {
    FirstView()
        .tabItem {
            Label("Everyone", systemImage: "person.3")
        }
    SecondView()
        .tabItem {
            Label("Contacted", systemImage: "checkmark.circle")
        }
    ThirdView()
        .tabItem {
            Label("Uncontacted", systemImage: "questionmark.diamond")
        }
    FourthView()
        .tabItem {
            Label("Me", systemImage: "person.crop.square")
        }
}

TabItem 的设置

tabItem 可以进行以下一些设置:

//tabItem 可以只有文字
.tabItem{
	Text("标签文案")
}

//tabItem 可以只有图标
.tabItem{
	Image(systemName:"phone")
}

//tabItem 可以是文字 + 图标,顺序无所谓
.tabItem{
	Image(systemName:"phone")
	Text("标签文案")
}

//tabItem 也可以用label来设置图标和文字
.tabItem{
	Label("Messages", systemImage: "phone.and.waveform.fill")
}

// 修改 tabItem 的颜色
TabView {
		Text("Second Screen")
		.tabItem { 
				Image(systemName: "moon.fill")
		}
		//在tabItem后面设置颜色,是无效的
		//.foregroundColor(Color.red)
} 
.edgesIgnoringSafeArea(.top)
.accentColor(.yellow)

// 想改变tabItem图标颜色的有效方法,是设置激活图标的颜色,用这个accentColor
// 注意这个修饰器是加到 TabView 后面的

向 tabItem 添加徽章:

// 例如,如果您想在选项卡项上显示红色数字 5,您可以使用以下命令:
TabView {
    Text("Your home screen here")
        .tabItem {
            Label("Home", systemImage: "house")
        }
        .badge(5)
}

以编程方式控制

  1. 创建状态属性储存当前 Tab
@State private var selectedTab = "One"
  1. 将状态属性绑定到 TabView

将状态属性作为绑定传递到 TabView 中,以便自动跟踪它。

TabView(selection: $selectedTab) { ... }
  1. 设置每个 TabItem 的 tag 值

接下来需要告诉 SwiftUI 应为该属性的每个值显示哪个选项卡,如何实现?我们为每个视图附加一个唯一的标识符,并将其用于选定的选项卡。这些标识符称为标签,并使用 tag() 修饰符附加。

Text("Tab 2")
.tabItem {
		Label("Two", systemImage: "circle")
}
.tag("Two")

<aside> 💡 选项卡的标签可以是任何你想要的,只要数据类型符合 Hashable ,整数可能效果很好。但是如果您要进行任何有意义的编程导航,您应该确保将标签放在某个固定位置,例如视图内的静态属性,这样可以让你在许多地方能共享标签值,降低出错的风险。

</aside>

  1. 想跳转时,修改状态属性

每当想要跳转到不同的选项卡时,将该状态属性修改为新值;

Button("Show Tab 2") {
    selectedTab = "Two"
}
.tabItem {
    Label("One", systemImage: "star")
}
// 完整例子:
@State private var selectedTab = "One"

var body: some View {
				//将 TabView 的选择绑定到 $selectedTab;当创建 TabView 时,它会作为参数传递
        TabView(selection: $selectedTab) {
            Button("Show Tab 2") {
                selectedTab = "Two"
            }
            .tabItem {
                Label("One", systemImage: "star")
            }
            .tag("One")

            Text("Tab 2")
            .tabItem {
		            Label("Two", systemImage: "circle")
            }
            .tag("Two")
        }
}

使用整数作为绑定值也是可以的:

当然,仅仅使用“One”和“Two”并不理想,这些值是固定的,它虽然解决了视图跳转的问题,但它们不容易记住。幸运的是,您可以使用您喜欢的任何值:为每个视图提供一个唯一并反映其用途的字符串标记,将其用于您的 @State 属性。从长远来看,建议使用整数。

// 状态参数,设置当前默认选中的标签索引
    @State private var selectedTab = 0
 
    var body: some View {

				// 告诉 TabView 绑定刚刚设置的状态参数
        TabView(selection: $selectedTab) {

						// 第一个Tab
            Text("Tab 1")

								// 底部的标签不需要添加行为,但如果希望点击页面内的其他元素也跳转到其他Tab
								// 那就要在和其他元素交互的时候,把状态参数 selectedTab 改变
                .onTapGesture {
                    self.selectedTab = 1
                }
                .tabItem {
                    Image(systemName: "star")
                    Text("One")
                }
								// 要给每个tagItem后面加上tag(),以便可以用编程的方式去跳转
                .tag(0)
 
						// 第二个Tab
            Text("Tab 2")
                .tabItem {
                    Image(systemName: "star.fill")
                    Text("Two")
                }
								// 要给每个tagItem后面加上tag(),以便可以用编程的方式去跳转
                .tag(1)
        }
    }

与 NavigationStack 共用

<aside> 💡 同时使用 NavigationStackTabView 是很常见的,但应该注意: TabView 应该用作父视图,其中某个选项卡,在必要时可以嵌入一个 NavigationStack ,而不能够反过来(把 NavigationStack 作为父视图)。

</aside>


隐藏系统默认 TabBar

如果想彻底隐藏 TabView 的默认 TabBar,并且确保它的响应区域(交互区域)完全消失,使用 SwiftUI 内置的手段(例如避免 .tabItem())可能并不总是足够,因为它可能会留下不可见但仍然可触发的交互区域。

为了彻底隐藏 TabBar 及其响应区域,可以通过 UIKit 的 UITabBar.appearance().isHidden = true 来全局隐藏 TabBar。这种方式直接操作底层的 UIKit,能够确保 TabBar 在整个应用中完全不可见和不可交互。这是目前最有效的方法来避免默认 TabView 中不可见但可交互的 TabBar

import SwiftUI

// 可以在 SwiftUI 的 App 或 View 的初始化过程中设置 UITabBar.appearance().isHidden = true 来隐藏 TabBar
@main
struct YourApp: App {
    init() {
        // 全局隐藏系统的 TabBar
        UITabBar.appearance().isHidden = true
    }
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

<aside> 💡

注意:使用 UITabBar.appearance().isHidden = true 是全局性的。如果你在应用的其他地方使用了 TabView,系统的 TabBar 也会被隐藏。因此,如果你只想在某些特定的页面隐藏 TabBar,而不是全局隐藏,你可能需要找到更细粒度的解决方案,比如自定义 UIViewControllerRepresentable 来仅在某些 TabView 中隐藏 TabBar

</aside>


.tabViewStyle 创建翻页页面

要激活页面视图样式,请将 .tabViewStyle() 修饰符附加到 TabView ,并传入 .page 。例如将以下代码添加到 @main Swift 文件中:

// 当它在 iOS、tvOS 和 watchOS 上运行时,您会发现可以滑动浏览页面列表。在 macOS 上,不支持 .page
TabView {
    Text("First")
    Text("Second")
    Text("Third")
    Text("Fourth")
}

// 设置分页样式
.tabViewStyle(.page)

// 设置分页点样式
// 警告:分页点是白色且半透明的白色,因此如果视图背景也是白色,可能看不到它们。
// 要解决此问题,可以通过在 tabViewStyle() 之后要求 SwiftUI 在后面放置背景
.indexViewStyle(.page(backgroundDisplayMode: .always))