iPadOS 18 顯示在上方的 tab bar 和顯示更多資訊的側邊欄

iPadOS 18 的 tab bar 變苗條了 ! 它變成細細的一條,而且還搬了個家,從下面搬到上面,讓 App 有更大的空間展示內容。(ps: iOS 18 的 tab bar 並沒有改變,還是維持佔據下方的空間)

iPadOS 18 的 Books App

除了外貌的改變,iPad 的 tab bar 還多了方便使用者查看更多內容的側選單。

以 News App 為例,點選下圖的紅色框框按鈕可叫出側選單。

側選單還可以分類,點選箭頭可展開關閉清單。

點選側邊欄的 edit 可進入編輯摸式調整 tab 的顯示和順序。

接下來讓我們從頭開始,一步步介紹 tab bar 在 iPadOS 18 的進化跟程式實作細節。

  • iPadOS 18 顯示在上方的 tab bar。
  • 使用 enum 定義 tab 的清單,文字和 icon。
  • 不限數量的 tab。
  • 顯示側邊欄的 sidebarAdaptable。
  • 隱藏 tab 的 defaultVisibility & tabPlacement。
  • 將多個 tab 變成群組的 TabSection。
  • 讓使用者客製 tab 的 TabViewCustomization。
  • 控制 TabView 顯示的 tab 分頁。
  • UIKit 的 UITabBarController。

iPadOS 18 顯示在上方的 tab bar

在 iPadOS 18 tab bar 變成顯示在上方,而且建立 tab 有了新的寫法,我們利用新的 Tab 建立 tab 分頁,使用它的 init(_:systemImage:content:) 設定 tab 的文字、icon 和顯示的頁面。(ps: 之前利用 tabItem(_:) 設定 tab 文字和 icon 的寫法將在未來 deprecated)

import SwiftUI

struct ContentView: View {
var body: some View {
TabView {
Tab("apple", systemImage: "apple.logo") {
Image(systemName: "apple.logo")
.resizable()
.scaledToFit()
}
Tab("swift", systemImage: "swift") {
Image(systemName: "swift")
.resizable()
.scaledToFit()
}
}
.foregroundStyle(Color.orange.mix(with: .white, by: 0.5))

}
}

剛剛的程式除了用到新的 Tab,還使用 iOS 18 新推出的混色方法 mix(with:by:in:) 混合橘色和白色。

iPadOS 18 的 tab 只會顯示文字,不過 icon 在其它平台還是有用的。如下圖所示,iPhone 下方 tab bar 的 tab 將同時顯示文字和 icon。

使用 enum 定義 tab 的清單,文字和 icon

擅長定義清單的 enum 很適合用來定義 tab 的清單,文字和 icon,以下我們將剛剛例子的 apple & swift 改成用 enum Tabs 定義,property name 是 tab 的文字,property symbol 是 tab 的 SF Symbol icon 名字。

import Foundation

enum Tabs {
case apple
case swift

var name: String {
switch self {
case .apple:
"蘋果"
case .swift:
"Swift"
}
}

var symbol: String {
switch self {
case .apple:
"apple.logo"
case .swift:
"swift"
}
}
}

import SwiftUI

struct ContentView: View {
var body: some View {
TabView {
Tab(Tabs.apple.name, systemImage: Tabs.apple.symbol) {
Image(systemName: "apple.logo")
.resizable()
.scaledToFit()
}
Tab(Tabs.swift.name, systemImage: Tabs.swift.symbol) {
Image(systemName: "swift")
.resizable()
.scaledToFit()
}
}
.foregroundStyle(Color.orange.mix(with: .white, by: 0.5))
}
}

不限數量的 tab

從前 iOS tab bar 的 tab 上限為 5 個,iPadOS 的上限為 8 個,在 iPadOS 18 終於進化成無上限,當數量太多時它會變成水平滑動的 tab bar 。

以下程式顯示 26 個英文字母的 tab,並且搭配 iOS 18 的 MeshGradient 顯示美麗的漸層。

import SwiftUI

struct ContentView: View {
let letters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

var body: some View {
TabView {
ForEach(letters, id: \.self) { letter in
let name = "\(letter).circle.fill"
Tab(letter, systemImage: name) {
Image(systemName: name)
.resizable()
.scaledToFit()
}
}
}
.foregroundStyle(
MeshGradient(width: 3, height: 3, points: [
.init(0, 0), .init(0.5, 0), .init(1, 0),
.init(0, 0.5), .init(0.5, 0.5), .init(1, 0.5),
.init(0, 1), .init(0.5, 1), .init(1, 1)
], colors: [
.red, .purple, .indigo,
.orange, .white, .blue,
.yellow, .green, .mint
])
)
}
}

顯示側邊欄的 sidebarAdaptable

tabViewStyle 在 iPadOS & iOS 18 多了 sidebarAdaptable,只要加上 .tabViewStyle(.sidebarAdaptable)即可讓 iPad 的 tab bar 多出側邊欄。sidebarAdaptable 在不同平台有不同影響,比方 iOS 將不會長出側邊欄,各平台的詳細差異可參考以下連結。

import SwiftUI

struct ContentView: View {
let letters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]

var body: some View {
TabView {
ForEach(letters, id: \.self) { letter in
let name = "\(letter).circle.fill"
Tab(letter, systemImage: name) {
Image(systemName: name)
.resizable()
.scaledToFit()
}
}
}
.tabViewStyle(.sidebarAdaptable)
.foregroundStyle(
MeshGradient(width: 3, height: 3, points: [
.init(0, 0), .init(0.5, 0), .init(1, 0),
.init(0, 0.5), .init(0.5, 0.5), .init(1, 0.5),
.init(0, 1), .init(0.5, 1), .init(1, 1)
], colors: [
.red, .purple, .indigo,
.orange, .white, .blue,
.yellow, .green, .mint
])
)
}
}

點選箭頭指向的按鈕顯示側邊欄。

直向模式時側邊欄和畫面的內容會部分重疊。

橫向模式時畫面將自動調整大小,左邊顯示側邊欄,右邊顯示漸層的字母。

隱藏 tab 的 defaultVisibility & tabPlacement

當 tab 太多時,我們也可以設定上方的 tab bar 只顯示重要的 tab,使用者想瀏覽其它 tab 頁面時再點選側邊欄查看。

以下程式上方的 tab bar 只顯示江湖上的名門正派少林、武當、峨眉,比較小的全真教和古墓派只能躲在側邊欄。

import SwiftUI

struct ContentView: View {
var body: some View {
TabView {
Tab("少林", systemImage: "star") {
SchoolView(school: "少林", imageResource: .coding1)
}

Tab("武當", systemImage: "star") {
SchoolView(school: "武當", imageResource: .coding1)
}

Tab("峨眉", systemImage: "star") {
SchoolView(school: "峨眉", imageResource: .coding1)
}

Tab("全真教", systemImage: "star") {
SchoolView(school: "全真教", imageResource: .coding1)
}
.defaultVisibility(.hidden, for: .tabBar)

Tab("古墓派", systemImage: "star") {
SchoolView(school: "古墓派", imageResource: .coding2)
}
.defaultVisibility(.hidden, for: .tabBar)
}
.tabViewStyle(.sidebarAdaptable)
}
}
import SwiftUI

struct SchoolView: View {
var school: String
var imageResource: ImageResource

var body: some View {
VStack {
Image(imageResource)
.resizable()
.scaledToFit()
Text(school)
.font(.system(size: 100))
}
}
}

利用 defaultVisibility(_:for:) 控制 tab 是否顯示,傳入 .hidden 隱藏 tab。參數 for 指定隱藏的地方,可傳入的選項有 .tarBar 和 .sidebar。

全真教和古墓派的 tab 呼叫 defaultVisibility(_:for:)時傳入 .tabBar,因此它們無緣出現在 tab bar,只能在 sidebar 露臉。

值得注意的,當參數 for 傳入 .sidebar 時 tab 在 sidebar & tabbar 都不會出現,因為依據 Apple 的設計,tab bar 顯示的 tab 也要出現在 sidebar,當 tab 沒有出現在 sidebar,表示它也不會顯示在 tab bar。

除了 defaultVisibility,我們也可以用 tabPlacement(.sidebarOnly) 設定 tab 只顯示在側邊欄,它跟 defaultVisibility 的差別將在等下介紹 tab 編輯功能時揭曉。

import SwiftUI

struct ContentView: View {

var body: some View {
TabView {
Tab("少林", systemImage: "star") {
SchoolView(school: "少林", imageResource: .coding1)
}

Tab("武當", systemImage: "star") {
SchoolView(school: "武當", imageResource: .coding1)
}

Tab("峨眉", systemImage: "star") {
SchoolView(school: "峨眉", imageResource: .coding1)
}

Tab("全真教", systemImage: "star") {
SchoolView(school: "全真教", imageResource: .coding1)
}
.tabPlacement(.sidebarOnly)

Tab("古墓派", systemImage: "star") {
SchoolView(school: "古墓派", imageResource: .coding2)
}
.tabPlacement(.sidebarOnly)

}
.tabViewStyle(.sidebarAdaptable)
}
}

ps: 剛剛介紹的 defaultVisibility 和 tabPlacement 並不影響 iPhone,在 iPhone 上如果想隱藏 tab,我們可透過 modifier hidden 設定,在 horizontalSizeClass 是 compact 時隱藏 tab。

import SwiftUI

struct ContentView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass

var body: some View {
TabView {
Tab("少林", systemImage: "star") {
SchoolView(school: "少林", imageResource: .coding1)
}

Tab("武當", systemImage: "star") {
SchoolView(school: "武當", imageResource: .coding1)
}

Tab("峨眉", systemImage: "star") {
SchoolView(school: "峨眉", imageResource: .coding1)
}

Tab("全真教", systemImage: "star") {
SchoolView(school: "全真教", imageResource: .coding1)
}
.hidden(horizontalSizeClass == .compact)

Tab("古墓派", systemImage: "star") {
SchoolView(school: "古墓派", imageResource: .coding2)
}
.hidden(horizontalSizeClass == .compact)
}
.tabViewStyle(.sidebarAdaptable)
}
}

struct SchoolView: View {
var school: String
var imageResource: ImageResource

var body: some View {
VStack {
Image(imageResource)
.resizable()
.scaledToFit()
Text(school)
.font(.system(size: 100))
}
}
}

將多個 tab 變成群組的 TabSection

當側邊欄顯示很多內容時,我們也可以利用 TabSection 包住多個 tab,將多個 tab 變成某個分類下的群組,例如下圖的全真教和古墓派歸屬在小門派下。

以下為實作的程式。

import SwiftUI

struct ContentView: View {
let schools = [
"少林",
"武當",
"峨眉",
"華山",
"崑崙",
]

var body: some View {
TabView {
ForEach(schools, id: \.self) { school in
Tab(school, systemImage: "star") {
SchoolView(school: school, imageResource: .coding1)
}
}

TabSection("小門派") {
Tab("全真教", systemImage: "star") {
SchoolView(school: "全真教", imageResource: .coding1)
}
Tab("古墓派", systemImage: "star") {
SchoolView(school: "古墓派", imageResource: .coding2)
}
}
.defaultVisibility(.hidden, for: .tabBar)
}
.tabViewStyle(.sidebarAdaptable)
}
}

我們從 TabSection 呼叫 defaultVisibility(.hidden, for: .tabBar),因此小門派不會出現在上方的 tab bar。

當 TabSection 沒有隱藏時,它在 tab bar 上只會變成一個 tab,即使它包含多個 tab。tab 的文字將是 TabSection 的標題,內容是它包含的第一個 tab。

import SwiftUI

struct ContentView: View {
let schools = [
"少林",
"武當",
"峨眉",
"華山",
"崑崙",
]

var body: some View {
TabView {
ForEach(schools, id: \.self) { school in
Tab(school, systemImage: "star") {
SchoolView(school: school, imageResource: .coding1)
}
}

TabSection("小門派") {
Tab("全真教", systemImage: "star") {
SchoolView(school: "全真教", imageResource: .coding1)
}
Tab("古墓派", systemImage: "star") {
SchoolView(school: "古墓派", imageResource: .coding2)
}
}
}
.tabViewStyle(.sidebarAdaptable)
}
}

以下圖為例,TabSection 在 tab bar 上顯示的標題是小門派,內容為 TabSection 包含的第一個 tab 全真教。

讓使用者客製 tab 的 TabViewCustomization

我們也可以加入 TabViewCustomization,讓使用者擁有客製 tab bar 的能力,控制顯示哪些 tab 和調整 tab 的順序。

以下我們先示範如何編輯 tab 再說明程式。當 tab 可以編輯時,側邊欄會出現 Edit 按鈕。

點選 Edit 後,使用者可做以下操作。

  • 從側邊欄勾選 tab 是否顯示。
  • 用拖曳的方式將上方 tab bar 的某個 tab 移除。
  • 用拖曳的方式將側邊欄的某個 tab 移到上方的 tab bar 。
  • 調整 tab 的順序,不過只有 TabSection 的 tab 能調整順序。

我們可以從程式設定某些 tab 不能編輯,因此下圖的少林 tab 不能編輯,而且調整順序的三條線只會出現在 TabSecton 的 tab。

以下我們試試取消峨眉的勾選和調整全真跟古墓的順序。

之後若想要峨眉出現在上方的 tab bar,必須透過兩個步驟。

  • 勾選峨眉,讓它出現在側邊欄。
  • 從側邊欄拖曳峨眉到上方的 tab bar。

以下為實作的程式。

import SwiftUI

struct ContentView: View {
@AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization

var body: some View {
TabView {
Tab("少林", systemImage: "star") {
SchoolView(school: "少林", imageResource: .coding1)
}
.customizationID("少林")
.customizationBehavior(.disabled, for: .sidebar, .tabBar)

Tab("武當", systemImage: "star") {
SchoolView(school: "武當", imageResource: .coding1)
}
.customizationID("武當")

Tab("峨眉", systemImage: "star") {
SchoolView(school: "峨眉", imageResource: .coding1)
}
.customizationID("峨眉")

TabSection("小門派") {
Tab("全真教", systemImage: "star") {
SchoolView(school: "全真教", imageResource: .coding1)
}
.customizationID("全真教")

Tab("古墓派", systemImage: "star") {
SchoolView(school: "古墓派", imageResource: .coding2)
}
.customizationID("古墓派")
}
.defaultVisibility(.hidden, for: .tabBar)
.customizationID("小門派")
}
.tabViewStyle(.sidebarAdaptable)
.tabViewCustomization($tabViewCustomization)
}
}

說明。

@AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization

當使用者調整 tab 的顯示和順序後,結果將存在 TabViewCustomization 。我們將它宣告成 AppStorage 物件,如此它才能永久儲存,下次打開 App 時載入之前的設定。

.customizationID("少林")

利用 customizationID 設定 Tab 和 TabSection 的 id,TabViewCustomization 將利用 id 識別 tab 和儲存 tab 的狀態。

.tabViewCustomization($tabViewCustomization)

讓 TabView 變成可編輯,並將編輯的結果存在前面宣告的 tabViewCustomization。

.customizationBehavior(.disabled, for: .tabBar, .sidebar)

利用 customizationBehavior 調整 tab 的行為。我們想讓重要的 tab 少林不能編輯,因此將它的行為設為 .disabled。參數 for 設定行為套用的地方,可傳入多個。在此我們傳入 .tabBar 和 .sidebar,因此 tab 少林在 tabBar 和 sideBar 都不能編輯。

前面提到 defaultVisibility(.hidden, for: .tabBar) & tabPlacement(.sidebarOnly) 都可讓 tab 只顯示在側邊欄,不過當 tab 可以編輯時,它們會有小小的差異。當 tab 設為 defaultVisibility(.hidden, for: .tabBar) 時,使用者還是能將它拖曳到 tab bar,tabPlacement(.sidebarOnly) 則會禁止這樣的操作。

控制 TabView 顯示的 tab 分頁

我們也可以從程式控制 TabView 顯示的分頁,方法如下。


import SwiftUI

struct ContentView: View {
@State private var selectedTab = "武當"

var body: some View {
TabView(selection: $selectedTab) {
Tab("少林", systemImage: "star", value: "少林") {
SchoolView(school: "少林", imageResource: .coding1)
}

Tab("武當", systemImage: "star", value: "武當") {
SchoolView(school: "武當", imageResource: .coding1)
}

Tab("峨眉", systemImage: "star", value: "峨眉") {
SchoolView(school: "峨眉", imageResource: .coding1)
}

TabSection("小門派") {
Tab("全真教", systemImage: "star", value: "全真教") {
Text("全真教")
}

Tab("古墓派", systemImage: "star", value: "古墓派") {
Text("古墓派")
}
}
.defaultVisibility(.hidden, for: .tabBar)
}
.tabViewStyle(.sidebarAdaptable)
}
}

說明

  • TabView init(selection:content:) 的 selection 參數決定顯示的 tab 分頁,我們將 selection 綁定 selectedTab,設定它的內容為武當。
  • TabView 如何知道 selectedTab 的內容對應到哪個 tab 呢? 答案就在 Tab init(_:systemImage:value:content:) 的參數 value,因此若想控制顯示某個 tab 分頁,Tab 還要額外設定 value。

App 執行後成功地顯示我們設定的武當分頁。

從剛剛的例子,我們看到當 tab 功能愈複雜時,需要設定的資訊也愈多,包含文字,icon,Tab 的 value 和 customizationID,因此我們也可將這些資訊統一整理在 enum ,然後用 enum 當 Tab 的 value。

以下我們改寫一開始的 apple & swift 範例,在 enum Tabs 加上 property id,在此我們使用 name 當 id。

import Foundation

enum Tabs {
case apple
case swift

var id: String {
name
}

var name: String {
switch self {
case .apple:
"蘋果"
case .swift:
"Swift"
}
}

var symbol: String {
switch self {
case .apple:
"apple.logo"
case .swift:
"swift"
}
}
}

使用 Tabs.swift 當 TabView 綁定的 selection 資料,讓它一開始顯示 swift 分頁。

import SwiftUI

struct ContentView: View {
@State private var selectedTab: Tabs = .swift
@AppStorage("sidebarCustomizations") var tabViewCustomization: TabViewCustomization

var body: some View {
TabView(selection: $selectedTab) {
Tab(Tabs.apple.name, systemImage: Tabs.apple.symbol, value: .apple) {
Image(systemName: "apple.logo")
.resizable()
.scaledToFit()
}
.customizationID(Tabs.apple.id)

Tab(Tabs.swift.name, systemImage: Tabs.swift.symbol, value: .swift) {
Image(systemName: "swift")
.resizable()
.scaledToFit()
}
.customizationID(Tabs.swift.id)

}
.tabViewStyle(.sidebarAdaptable)
.tabViewCustomization($tabViewCustomization)
.foregroundStyle(Color.orange.mix(with: .white, by: 0.5))
}
}

UIKit 的 UITabBarController

UIKit 的 UITabBarController 在 iOS & iPadOS 18 也多了新的寫法,建立 tab 分頁可利用全新的 UITab。

tab bar controller 在 iPadOS 18 也支援側邊欄,只要將新的 property mode 設為 .tabSidebar。

 tabBarController?.mode = .tabSidebar

參考連結: Apple 官方範例 Enhancing your app’s content with tab navigation

學會以上 iPadOS 18 tab bar 的新功能和相關程式後,有興趣的朋友可再進一步參考以下 Apple 的官方範例。

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com