概览
看来苹果一直对 iPadOS 中标签栏(TabView)不甚满意。这不,在 WWDC 2024 中苹果又对 TabView 外观做了大幅度的进化。
现在我们可以在顶部悬浮条和左侧的 Sidebar 两种不同布局之间恣意切换 TabView 的外观啦。而且,这在 SwiftUI 6.0 中显得尤为简单。
在本篇博文中,您将学到如下内容:
- 概览
- [1. iPadOS 18 中全新的 Sidebar+悬浮 TabView 样式](#1. iPadOS 18 中全新的 Sidebar+悬浮 TabView 样式)
- [2. 新 TabView 样式在 iOS 18 中的表现](#2. 新 TabView 样式在 iOS 18 中的表现)
- [3. SwiftUI 6.0 新的 Tab 和 TabSection 原生视图](#3. SwiftUI 6.0 新的 Tab 和 TabSection 原生视图)
- [4. 让 Tab 懂你所选](#4. 让 Tab 懂你所选)
- [5. 调整 Tab 的显示位置](#5. 调整 Tab 的显示位置)
- 总结
看来 iPadOS 18 对 TabView 外观和功能都做了重量级的升级,那小伙伴们还等什么呢?
Let's find out!!!😉
1. iPadOS 18 中全新的 Sidebar+悬浮 TabView 样式
为了最大化利用屏幕空间,从 iPadOS 18 开始苹果有史以来第一次将位于底部的 TabView:
神奇的变为悬浮条置于窗口的顶部:
从上面两张图中我们可以对比在 iPadOS 18 中系统默认的时钟 App 中标签栏的新外观。
当需要显示悬浮条内额外标签页的内容时,我们还可以轻按最左侧的展开按钮将其变形为 Sidebar 模式以便将所有标签项一览无遗:
不仅如此,我们还可以恣意调整 Sidebar 和悬浮条中标签项的位置、将它们打包到组中显示,甚至定制它们的外观:
按苹果的话来说:标签栏的新外观可以最大化利用显示空间,并让我们的 App 极具现代化外观的范儿。 我们可以在悬浮条和 Sidebar 两者间随意切换从而解决了"麻雀虽小"和"面面俱到"之间鱼与熊掌不可兼得的问题。
2. 新 TabView 样式在 iOS 18 中的表现
为了能保持 iPadOS 18 中新 TabView 的延展性,在 iOS 18 中新的标签栏会"退化"为之前经典的样式。
与之前经典 TabView 外观类似:新的 TabView 在 iOS 18 中右侧额外显示不下的内容也会统统放到单独的 More 标签项中去。
所不同的是,iOS 18 新标签栏中的标签组(TabSection)会消失的"无影无踪",其中的标签会被直接放在底部的标签栏中。
有些小伙伴们可能会好奇:iPhone pro max 大屏设备横屏时新的 TabView 会有怎样的表现?
上面就是 iPhone pro max 横屏时新标签栏在 iOS 18 中的样子。可以看到:除了最右侧 More 标签中的内容被平铺显示以外和之前并无二致。所以想利用大屏 iPhone 设备适配新 TabView 悬浮条外观的小伙伴们可能要暂时失望了。
了解完 iOS 18 中新标签栏的模样之后,下面就让我们来看看如何用代码展现它们吧。
在 SwiftUI 6.0 中适配 TabView 新外观超级简单,然我们马上来撸码为证!
3. SwiftUI 6.0 新的 Tab 和 TabSection 原生视图
从 SwiftUI 6.0 开始,苹果增加了全新的 Tab 和 TabSection 原生视图,专门为新 TabView 来效"犬马之劳":
其中,Tab 被用来取代原来的 tabItem() 修改器,而 TabSection 则用来将多个 Tab 汇聚成组。
在 iPadOS 18 之前,我们是这样描述 TabView 外观的:
swift
struct ContentView: View {
@Environment(\.horizontalSizeClass) var hSize
@State var tabSelecting = TabValue.allTopics
@State var moveFromLeading = true
var body: some View {
TabView(selection: $tabSelecting) {
AnimSubView(title: "Main", bgColor: .white, imageName: "main", isMoveFromLeading: moveFromLeading)
.tabItem {
Label("Main", systemImage: "house")
}
.tag(TabValue.main)
AnimSubView(title: "This is the blog page", bgColor: .yellow, isMoveFromLeading: moveFromLeading)
.tabItem {
Label("Topics", systemImage: "pencil")
}
.tag(TabValue.allTopics)
AnimSubView(title: "SwiftUI topic", bgColor: .purple.opacity(0.3), isMoveFromLeading: moveFromLeading)
.tabItem {
Label("SwiftUI", systemImage: "swift")
}
.tag(TabValue.swiftUI)
AnimSubView(title: "Concurrency topic", bgColor: .green, isMoveFromLeading: moveFromLeading)
.tabItem {
Label("Concurrency", systemImage: "timelapse")
}
.tag(TabValue.concurrency)
AnimSubView(title: "Persistence topic", bgColor: .blue.opacity(0.3), isMoveFromLeading: moveFromLeading)
.tabItem {
Label("Persistence", systemImage: "swiftdata")
}
.tag(TabValue.persistence)
AnimSubView(title: "Search the site", bgColor: .cyan, isMoveFromLeading: moveFromLeading)
.tabItem {
Label("Search", systemImage: "magnifyingglass")
}
.tag(TabValue.search)
}
.safeAreaInset(edge: .bottom) {
Text("当前选中:\(tabSelecting)")
.font(.title2.weight(.heavy))
.foregroundStyle(.white.gradient)
.padding()
.background(RoundedRectangle(cornerRadius: 15).foregroundStyle(.mint.gradient))
.shadow(radius: 3.0)
}
.safeAreaPadding(.bottom, hSize == .compact ? 100 : 0)
.onChange(of: tabSelecting) {old,new in
// 为标签切换增加转场动画支持...
}
.tint(.pink)
}
}
可以看到:在 TabView 中我们使用 tabItem() 和 tag() 修改器用来构建每一个标签页。
而在新的 iPadOS 18 和 iOS 18 中,我们彻底抛弃了这两个修改器而改为使用 Tab 和 TabSection 视图来描述每个标签页:
swift
struct ContentView: View {
@State var moveFromLeading = true
var body: some View {
TabView {
Tab("Main", systemImage: "house") {
AnimSubView(title: "Main", bgColor: .white, imageName: "main", isMoveFromLeading: moveFromLeading)
}
TabSection("Blog") {
Tab("All topics", systemImage: "pencil") {
AnimSubView(title: "This is the blog page", bgColor: .yellow, isMoveFromLeading: moveFromLeading)
}
Tab("SwiftUI", systemImage: "swift") {
AnimSubView(title: "SwiftUI topic", bgColor: .purple.opacity(0.3), isMoveFromLeading: moveFromLeading)
}
Tab("Concurrency", systemImage: "timelapse") {
AnimSubView(title: "Concurrency topic", bgColor: .green, isMoveFromLeading: moveFromLeading)
}
Tab("Persistence", systemImage: "swiftdata") {
AnimSubView(title: "Persistence topic", bgColor: .blue.opacity(0.3), isMoveFromLeading: moveFromLeading)
}
}
// 更多的 TabSections
Tab(role: .search) {
AnimSubView(title: "Search the site", bgColor: .cyan, isMoveFromLeading: moveFromLeading)
}
}
.tint(.pink)
}
}
小伙伴们可以留意一下,在上面的代码中我们是如何使用 TabSection 将若干 Tab 聚合成组的。
运行可以看到,原本底部的标签栏化身为悬浮条一跃横空出世了:
要想让标签栏进一步支持 Sidebar 切换显示,我们只需将 .sidebarAdaptable 样式应用在 TabView 上即可:
swift
TabView {
...
}
.tabViewStyle(.sidebarAdaptable)
值得注意的是:要想 iOS 18 之前的标签栏旧语法支持悬浮条显示,我们无需修改半行代码。
而只需一行 tabViewStyle(.sidebarAdaptable) 语句的加持,我们也同样可以让旧标签栏支持 Sidebar 样式。
4. 让 Tab 懂你所选
现在我们新的 TabView 已经灵动的"跃然于纸上"了,不过如果我们希望动态跟踪用户选择了哪个 Tab 又该如何是好呢?
在 iOS 18 中使用 tag() 标识标签页的方式已经被废弃,取而代之的是全新的包含 value 参数的 Tag 构造器方法:
注意该 Tab 构造器必须放在 TabView(selection:) 特定的构造器闭包中才能编译通过。
swift
struct ContentView: View {
@Environment(\.horizontalSizeClass) var hSize
@State var tabSelecting = TabValue.allTopics
@State var moveFromLeading = true
var body: some View {
TabView(selection: $tabSelecting) {
Tab("Main", systemImage: "house", value: .main) {
AnimSubView(title: "Main", bgColor: .white, imageName: "main", isMoveFromLeading: moveFromLeading)
}
TabSection("Blog") {
Tab("All topics", systemImage: "pencil", value: TabValue.allTopics) {
AnimSubView(title: "This is the blog page", bgColor: .yellow, isMoveFromLeading: moveFromLeading)
}
Tab("SwiftUI", systemImage: "swift", value: .swiftUI) {
AnimSubView(title: "SwiftUI topic", bgColor: .purple.opacity(0.3), isMoveFromLeading: moveFromLeading)
}
Tab("Concurrency", systemImage: "timelapse", value: .concurrency) {
AnimSubView(title: "Concurrency topic", bgColor: .green, isMoveFromLeading: moveFromLeading)
}
Tab("Persistence", systemImage: "swiftdata", value: .persistence) {
AnimSubView(title: "Persistence topic", bgColor: .blue.opacity(0.3), isMoveFromLeading: moveFromLeading)
}
}
Tab(value: .search, role: .search) {
AnimSubView(title: "Search the site", bgColor: .cyan, isMoveFromLeading: moveFromLeading)
}
}
.tabViewStyle(.sidebarAdaptable)
.safeAreaInset(edge: .bottom) {
Text("当前选中:\(tabSelecting)")
.font(.title2.weight(.heavy))
.foregroundStyle(.white.gradient)
.padding()
.background(RoundedRectangle(cornerRadius: 15).foregroundStyle(.mint.gradient))
.shadow(radius: 3.0)
}
.safeAreaPadding(.bottom, hSize == .compact ? 100 : 0)
.onChange(of: tabSelecting) {old,new in
// 标签切换转场动画支持代码从略...
}
.tint(.pink)
}
}
现在,我们已经能够捕获到用户当前选择的标签页啦:
5. 调整 Tab 的显示位置
除了在 TabView 中用 Tab 和 TabSection 妥善放置各个标签页以外,我们还可以进一步决定它们到底能够在何处显示。
这是通过 tabPlacement() 修改器方法来实现的:
其中对于 TabPlacement 类型的参数值我们有 3 种选择:
比如假若我们希望将 TabSection("Blog") 内部中间两个 Tab 始终放在悬浮条里,我们可以分别为其调用 .tabPlacement(.pinned) 修改器方法:
swift
TabSection("Blog") {
Tab("All topics", systemImage: "pencil", value: TabValue.allTopics) {
AnimSubView(title: "This is the blog page", bgColor: .yellow, isMoveFromLeading: moveFromLeading)
}
Tab("SwiftUI", systemImage: "swift", value: .swiftUI) {
AnimSubView(title: "SwiftUI topic", bgColor: .purple.opacity(0.3), isMoveFromLeading: moveFromLeading)
}
.tabPlacement(.pinned)
Tab("Concurrency", systemImage: "timelapse", value: .concurrency) {
AnimSubView(title: "Concurrency topic", bgColor: .green, isMoveFromLeading: moveFromLeading)
}
.tabPlacement(.pinned)
Tab("Persistence", systemImage: "swiftdata", value: .persistence) {
AnimSubView(title: "Persistence topic", bgColor: .blue.opacity(0.3), isMoveFromLeading: moveFromLeading)
}
}
现在,这两个标签项会一如既往的驻留在顶部的悬浮条中,棒棒哒:
总结
在本篇博文中,我们介绍了 SwiftUI 6.0(iPadOS 18/iOS 18)新标签页中新增的 Tab 和 TabSection 原生视图,并进一步讨论了如何让 TabView 支持 Sidebar 样式以及其它有趣的新特性。
感谢观赏,再会!😎