<aside>
💡 SwiftUI 中的文件名不要起的跟常用的View的名字一样,例如一个文件名叫TabView
的文件,里面又用了TabView(){ … }
这个视图,会莫名其妙报错。
</aside>
NavigationStack
非常适合创建分层视图堆栈,让用户深入了解数据,但它们不太适合展示不相关的数据。想要展示一些不相关的视图,经常需要使用 TabView
,它会在屏幕底部创建一个按钮条,点击每个按钮会显示不同的视图。TabView
就像是其自身内部的子视图的容器。这些子视图是单独的屏幕,它提供标签按钮 TabItems
,允许用户在这些子视图之间切换;当标签太多,无法满足设备的需求时,就会创建 "更多 "按钮,在这里可以找到其余的标签。
.tabItem
修饰器,用以在底部加上一个tab标签,指向这个视图对于 iOS 18 及更高版本, TabView
是从多个单独的 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)
具体的外观取决于用户使用的设备以及他们激活的选项卡视图模式。
Tab
。您需要确保他们有访问选项卡内容的替代方法如果想以编程方式控制选项卡选择,请绑定到 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 及更早版本中,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{
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)
}
@State private var selectedTab = "One"
将状态属性作为绑定传递到 TabView
中,以便自动跟踪它。
TabView(selection: $selectedTab) { ... }
接下来需要告诉 SwiftUI 应为该属性的每个值显示哪个选项卡,如何实现?我们为每个视图附加一个唯一的标识符,并将其用于选定的选项卡。这些标识符称为标签,并使用 tag()
修饰符附加。
Text("Tab 2")
.tabItem {
Label("Two", systemImage: "circle")
}
.tag("Two")
<aside>
💡 选项卡的标签可以是任何你想要的,只要数据类型符合 Hashable
,整数可能效果很好。但是如果您要进行任何有意义的编程导航,您应该确保将标签放在某个固定位置,例如视图内的静态属性,这样可以让你在许多地方能共享标签值,降低出错的风险。
</aside>
每当想要跳转到不同的选项卡时,将该状态属性修改为新值;
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)
}
}
<aside>
💡 同时使用 NavigationStack
和 TabView
是很常见的,但应该注意: TabView
应该用作父视图,其中某个选项卡,在必要时可以嵌入一个 NavigationStack
,而不能够反过来(把 NavigationStack
作为父视图)。
</aside>
如果想彻底隐藏 TabView
的默认 TabBar
,并且确保它的响应区域(交互区域)完全消失,使用 SwiftUI 内置的手段(例如避免 .tabItem()
)可能并不总是足够,因为它可能会留下不可见但仍然可触发的交互区域。
为了彻底隐藏 TabBar
及其响应区域,可以通过 UIKit 的 UITabBar.appearance().isHidden = true
来全局隐藏 TabBar
。这种方式直接操作底层的 UIKit,能够确保 TabBar
在整个应用中完全不可见和不可交互。这是目前最有效的方法来避免默认 TabView
中不可见但可交互的 TabBar
。
UITabBar.appearance().isHidden = true
:这一行代码会在应用的全局范围内隐藏所有 UITabBar
实例,包括 TabView
默认的 TabBar
。这确保了系统的 TabBar
不仅在视觉上消失,而且不会占用任何交互区域TabBar
后,你可以自定义 TabBarView
:你依然可以通过 SwiftUI 自定义自己的 TabBar
,并根据 selectedTab
进行视图切换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()
修饰符附加到 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))