WWDC Quick Look 💓 By SwiftGGTeam
Elevate your tab and sidebar experience in iPadOS

Elevate your tab and sidebar experience in iPadOS

观看原视频

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 引入了 TabTabSection 语法,UIKit 引入了 UITabUITabGroup。它们都用于统一描述 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 controller
  • identifier 用于追踪 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 会立即更新 UI
  • identifier 用于追踪分组身份,支持自定义功能
  • mode 属性决定显示模式,可动态切换

Sidebar 还支持为分组添加操作按钮,比如播客 App 中的”新建电台”。SwiftUI 中使用 sectionActions,UIKit 中使用 sidebarActions07: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 中启用自定义需要设置 allowsHidingpreferredPlacement12: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 控制用户能否隐藏该 tab
  • preferredPlacement 决定 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 上保持一致。


核心启发

  1. 用新版 SDK 编译验证外观变化

    • 为什么值得做:iPadOS 18 的 Tab Bar 外观变化无需代码改动即可生效,先编译看看效果是最快的验证方式
    • 怎么开始:将项目的 deployment target 设为 iPadOS 18,用 Xcode 16 编译运行,观察 Tab Bar 的自动升级效果
  2. 使用 SF Symbols 的 outline 变体

    • 为什么值得做:系统会自动处理 Tab Bar 和 Sidebar 的图标差异,不需要准备两套图片
    • 怎么开始:在 TabUITab 中使用 systemName 参数,选择 outline 版本的符号(如 square.grid.2x2
  3. 为搜索功能使用专用 API

    • 为什么值得做:Tab(role: .search)UISearchTab 自动获得固定位置和默认图标,符合用户对搜索功能的预期位置
    • 怎么开始:将现有的搜索 tab 替换为这些专用 API,移除手动的图标和标题设置
  4. 控制 tab 数量,用 Sidebar 承载更多内容

    • 为什么值得做:Tab Bar 中的 tab 过多会让用户难以定位,Sidebar 可以容纳更多层级而不显得拥挤
    • 怎么开始:将次要的、分类型的内容移到 TabSectionUITabGroup 中,保持 Tab Bar 精简
  5. 为重要 tab 禁用自定义

    • 为什么值得做:核心功能入口应该始终可见,避免用户误操作隐藏
    • 怎么开始:用 customizationBehavior(.disabled)preferredPlacement = .fixed 保护关键 tab

关联 Session

评论

GitHub Issues · utterances