Highlight
iPadOS 18 的 Tab Bar 移至屏幕顶部与导航栏共享空间,更紧凑的设计为内容腾出更多区域,用 iPadOS 18 SDK 编译的现有 App 无需代码改动即可获得新外观。
核心内容
iPad 上的 Tab Bar 自 iOS 诞生之初就存在,但在 iPadOS 18 中迎来了第一次重大改版。原有的 Tab Bar 位于屏幕底部,占用垂直空间;新设计将它移至顶部,与导航栏共享同一安全区域。这不仅让内容区域更宽敞,也使得 Tab 控件与其他导航元素(如返回按钮、标题)的距离更近,操作起来更顺手。
另一个长期存在的痛点是 Sidebar 的构建方式。在 iPadOS 17 及更早版本,开发者需要用 UISplitViewController 配合 Outline View 来实现 Sidebar。这种做法繁琐且不够直观。iPadOS 18 提供了更直接的解决方案:同一个 Tab 层级描述可以同时呈现为 Tab Bar 和 Sidebar。用户点击展开按钮时,Tab Bar 平滑过渡为 Sidebar,显示更多层级的内容(如文件夹、播放列表);收起后又回到紧凑的 Tab Bar。两种形态共享同一套状态数据,切换时无缝衔接。
个性化功能是这个新导航系统的另一个重点。用户可以从 Sidebar 中拖拽 tab 到 Tab Bar 进行固定,也可以将不需要的 tab 隐藏。Tab Bar 被划分为三个区域:固定区(不可自定义)、可定制区(可拖拽重排)、钉选区(始终在尾部可见,如搜索)。这些自定义行为会被系统自动持久化,下次打开 App 时恢复。
为了支持这些新特性,SwiftUI 引入了 Tab 和 TabSection 语法,UIKit 引入了 UITab 和 UITabGroup。它们都用于统一描述 App 的导航层级结构,而不是分别维护两套导航逻辑。
详细内容
SwiftUI 在 iOS 18 中更新了 TabView 的语法。不再使用旧的 tag/selection 模式,而是直接声明 Tab 结构体。每个 Tab 包含标题、图标和内容视图,可选地传入一个 selection value 用于编程控制选中状态。新语法在编译时就能检查类型一致性,避免了 runtime 错误(04:27)。
TabView {
Tab("Watch Now", systemImage: "play") {
WatchNowView()
}
Tab("Library", systemImage: "books.vertical") {
LibraryView()
}
// ...
}
UIKit 也提供了对应的 API。创建 UITab 实例来描述每个顶级 section,然后赋值给 tabBarController.tabs。对 UITab 实例的修改会立即反映在界面上(04:58)。
tabBarController.tabs = [
UITab(title: "Watch Now", image: UIImage(systemName: "play"), identifier: "Tabs.watchNow") { _ in
WatchNowViewController()
},
UITab(title: "Library", image: UIImage(systemName: "books.vertical"), identifier: "Tabs.library") { _ in
LibraryViewController()
},
// ...
]
关键点:
UITab是一个轻量级描述结构,不是 view controlleridentifier用于追踪 tab 的唯一身份,与自定义功能相关- 闭包中的
_参数代表UITab自身,可用于后续引用
搜索功能在各 App 中都很常见。SwiftUI 提供了 Tab(role: .search),UIKit 提供了 UISearchTab。使用这些专用 API,系统会自动配置默认的标题、放大镜图标,并将搜索 tab 固定在 Tab Bar 的尾部(05:58)。
// SwiftUI
Tab(role: .search) {
SearchView()
}
// UIKit
let searchTab = UISearchTab {
SearchViewController()
}
关键点:
- 搜索 tab 自动获得固定位置(pinned placement)
- 无需手动设置图标和标题,系统已内置
- 在 Sidebar 中也会显示,位置遵循正常排序规则
要启用 Sidebar,SwiftUI 中只需设置 tabViewStyle 为 .sidebarAdaptable。然后用 TabSection 将相关的 tab 分组。在 Sidebar 中,TabSection 会显示为一个可展开/收起的分组;在 Tab Bar 中,分组内的 tab 会平铺显示(06:41)。
TabView {
Tab("Watch Now", systemImage: "play") {
// ...
}
Tab("Library", systemImage: "books.vertical") {
// ...
}
// ...
TabSection("Collections") {
Tab("Cinematic Shots", systemImage: "list.and.film") {
// ...
}
Tab("Forest Life", systemImage: "list.and.film") {
// ...
}
// ...
}
TabSection("Animations") {
// ...
}
Tab(role: .search) {
// ...
}
}
.tabViewStyle(.sidebarAdaptable)
关键点:
TabSection在 Sidebar 中显示为分组标题- Tab 按声明顺序排列,Section 排在单个 tab 之后
- 搜索 tab 的位置不受 Section 影响,由系统决定固定位置
UIKit 中启用 Sidebar 需要将 tabBarController.mode 设为 .tabSidebar。使用 UITabGroup 表示分组,它的 children 属性可以动态更新(07:16)。
let collectionsGroup = UITabGroup(
title: "Collections",
image: UIImage(systemName: "folder"),
identifier: "Tabs.CollectionsGroup"
children: self.collectionsTabs()) { _ in
// ...
}
tabBarController.mode = .tabSidebar
tabBarController.tabs = [
UITab(title: "Watch Now", ...) { _ in
// ...
},
UITab(title: "Library", ...) { _ in
// ...
},
// ...
collectionsGroup,
UITabGroup(title: "Animations", ...) { _ in
// ...
},
UISearchTab { _ in
// ...
},
]
关键点:
UITabGroup是可变对象,修改children会立即更新 UIidentifier用于追踪分组身份,支持自定义功能mode属性决定显示模式,可动态切换
Sidebar 还支持为分组添加操作按钮,比如播客 App 中的”新建电台”。SwiftUI 中使用 sectionActions,UIKit 中使用 sidebarActions(07:45)。
TabSection(...) {
// ...
}
.sectionActions {
Button("New Station", ...) {
// action
}
}
// UIKit
let tabGroup = UITabGroup(...)
tabGroup.sidebarActions = [
UIAction(title: "New Station", ...) { _ in
// action
},
]
关键点:
- 操作按钮显示在分组标题的右侧
- 点击操作不会改变当前选中的 tab
- 可以为不同的分组配置不同的操作
Tab 还可以作为拖放目标。在相册 App 中,用户可以将照片拖到一个集合 tab 上,直接添加到该集合。SwiftUI 中使用 dropDestination modifier,UIKit 中实现 UITabBarControllerDelegate 的两个方法(08:12)。
// SwiftUI
Tab(collection.name, image: collection.image) {
CollectionDetailView(collection)
}
.dropDestination(for: Photo.self) { photos in
// Add 'photos' to the specified collection
}
// UIKit
func tabBarController(
_ tabBarController: UITabBarController,
tab: UITab, operationForAcceptingItemsFrom dropSession: any UIDropSession
) -> UIDropOperation {
session.canLoadObjects(ofClass: Photo.self) ? .copy : .cancel
}
func tabBarController(
_ tabBarController: UITabBarController,
tab: UITab, acceptItemsFrom dropSession: any UIDropSession) {
session.loadObjects(ofClass: Photo.self) { photos in
// Add 'photos' to the specified collection
}
}
关键点:
- 第一个方法判断能否接受拖放内容
- 第二个方法接收实际的数据对象
- 拖放操作在 Tab Bar 和 Sidebar 中都生效
用户自定义是这次更新的一大亮点。要启用自定义功能,SwiftUI 中需要给 TabView 附加一个 TabViewCustomization,并用 @AppStorage 持久化状态(10:45)。
@AppStorage("MyTabViewCustomization")
private var customization: TabViewCustomization
TabView {
Tab("Watch Now", systemImage: "play", value: .watchNow) {
// ...
}
.customizationID("Tab.watchNow")
// ...
TabSection("Collections") {
ForEach(MyCollectionsTab.allCases) { tab in
Tab(...) {
// ...
}
.customizationID(tab.customizationID)
}
}
.customizationID("Tab.collections")
// ...
}
.tabViewCustomization($customization)
关键点:
customizationID是 tab 参与自定义的唯一标识@AppStorage确保自定义状态在 App 重启后恢复value参数用于编程式选中某个 tab
某些 tab 不应该被用户自定义,比如核心功能入口。可以使用 customizationBehavior 来禁用自定义(11:40)。
Tab("Watch Now", systemImage: "play", value: .watchNow) {
// ...
}
.customizationBehavior(.disabled, for: .sidebar, .tabBar)
Tab("Optional Tab", ...) {
// ...
}
.customizationID("Tab.example.optional")
.defaultVisibility(.hidden, for: .tabBar)
关键点:
.disabled表示该 tab 在 sidebar/tabBar 中不可自定义.defaultVisibility(.hidden)让 tab 默认隐藏,但可被用户添加- 不参与自定义的 tab 不需要
customizationID
UIKit 中启用自定义需要设置 allowsHiding 和 preferredPlacement(12:38)。
let myTab = UITab(...)
myTab.allowsHiding = true
print(myTab.isHidden)
// .default, .optional, .movable, .pinned, .fixed, .sidebarOnly
myTab.preferredPlacement = .fixed
let myTabGroup = UITabGroup(...)
myTabGroup.allowsReordering = true
myTabGroup.displayOrderIdentifiers = [...]
关键点:
allowsHiding控制用户能否隐藏该 tabpreferredPlacement决定 tab 的默认位置和行为displayOrderIdentifiers记录用户自定义的顺序
UIKit 通过 delegate 方法通知自定义变化(12:39)。
func tabBarController(_ tabBarController: UITabBarController, visibilityDidChangeFor tabs: [UITab]) {
// Read 'tab.isHidden' for the updated visibility.
}
func tabBarController(_ tabBarController: UITabBarController, displayOrderDidChangeFor group: UITabGroup) {
// Read 'group.displayOrderIdentifiers' for the updated order.
}
关键点:
- 第一个方法在 tab 的可见性变化时调用
- 第二个方法在分组的显示顺序变化时调用
- 可在这些方法中同步到其他 UI 或数据模型
关于图标样式:Tab Bar 偏好填充风格的 glyph,Sidebar 偏好轮廓风格。使用 SF Symbols 时只需提供 outline 变体,系统会自动在 Tab Bar 中切换到 filled 变体(05:25)。这一行为在 iPad、iPhone 和 visionOS 上保持一致。
核心启发
-
用新版 SDK 编译验证外观变化
- 为什么值得做:iPadOS 18 的 Tab Bar 外观变化无需代码改动即可生效,先编译看看效果是最快的验证方式
- 怎么开始:将项目的 deployment target 设为 iPadOS 18,用 Xcode 16 编译运行,观察 Tab Bar 的自动升级效果
-
使用 SF Symbols 的 outline 变体
- 为什么值得做:系统会自动处理 Tab Bar 和 Sidebar 的图标差异,不需要准备两套图片
- 怎么开始:在
Tab或UITab中使用systemName参数,选择 outline 版本的符号(如square.grid.2x2)
-
为搜索功能使用专用 API
- 为什么值得做:
Tab(role: .search)和UISearchTab自动获得固定位置和默认图标,符合用户对搜索功能的预期位置 - 怎么开始:将现有的搜索 tab 替换为这些专用 API,移除手动的图标和标题设置
- 为什么值得做:
-
控制 tab 数量,用 Sidebar 承载更多内容
- 为什么值得做:Tab Bar 中的 tab 过多会让用户难以定位,Sidebar 可以容纳更多层级而不显得拥挤
- 怎么开始:将次要的、分类型的内容移到
TabSection或UITabGroup中,保持 Tab Bar 精简
-
为重要 tab 禁用自定义
- 为什么值得做:核心功能入口应该始终可见,避免用户误操作隐藏
- 怎么开始:用
customizationBehavior(.disabled)或preferredPlacement = .fixed保护关键 tab
关联 Session
- What’s new in SwiftUI — 介绍 TabView 的新语法和 iPadOS 上的外观变化
- Enhance your UI animations and transitions — 探索导航和展示中的动画过渡
- Create custom visual effects with SwiftUI — 学习如何在 SwiftUI 中创建视觉效果
- Create custom hover effects in visionOS — 了解 visionOS 中的交互设计
- Add personality to your app through UX writing — 探讨如何通过文案塑造 App 个性
评论
GitHub Issues · utterances