像 Wikipedia 和 Facebook 這樣多模式 App,會使用 TabBar 介面來讓使用者從不同操作模式中切換,例如 Wikipedia 上有 History、Places、Saved、Search 四個 Tab。一般來說,我們會將 TabBar 與 UITabBarController
物件結合使用,但也可以在 App 中將它們用作獨立控件。TabBar 總是出現在螢幕最底部,並顯示一個或多個 UITabBarItem
物件。
就如你所期待,SwiftUI 大幅地簡化了 TabView 的管理流程。在 WWDC 2019 的 SwiftUI Essentials 演講中介紹了 TabbedView 元件,其底下每個 TabItem 都導向至一個 SplitView。不幸的是,當我發現這項新元件時,Apple 已在 WWDC 之後變更了 TabbedView,也更改了 Tab 的處理方式 ── 他們以 TabView 元件取代 TabbedView。
在這篇文章中,我將分享如何建立一個 TabView,並連結 TabItem 到 SplitView 上。
使用 TabBar 的範例 App
想像一下,你想要建構一個 App 讓使用者記錄家庭旅遊,並記下每次旅遊的細節。同時,你也想讓使用者可以看看到過的地方的照片。以下就是這個 App 的 TabBar:

我已經為這個 App 建立了一個簡單的 XCode 專案。以下是 ContentView 的程式碼:
struct ContentView: View {
var body: some View {
VStack {
Text(verbatim:"Family Outings")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.primary)
TabView {
TripsView(trips: trips).tabItem {
NavigationLink(destination: TripsView(trips: trips)) {
Image(systemName: "car")
Text("Trips") }.tag(1)
}
PlacesView(places:places).tabItem {
NavigationLink(destination: PlacesView(places:places)) {
Image(systemName: "photo")
Text("Places") }.tag(2)
}
}
}
}
}
如你所見,這個 ContentView 是由具有兩個視圖的 VStack
所組成:
- 一個簡單的
Text
視圖,包含了主畫面的標題。 - 一個有兩個 Tab 的 TabView
看看 TabView 的第一個項目,它是由幾個部分組成的。首先,有個調用 tabitem
呼叫的視圖宣告 TripsView(trips:trips)
,它的效果是為 TripsView
設定 TabItem。之後,有一個包含 NavigationLink
的閉包,它用來導向目的地 TripsView
,而這個 Link 是由 Tab 的圖片及標題組成的。
啟動 App 來顯示 TripsView
的內容。

App 的預設動作是選擇第一個 TabItem。有一個版本的 TabView
可以讓你指定選擇的項目,不過要注意的是這需要一個 Bindable 物件。
點擊 Places Tab 來顯示地點列表。

以上就是關於 TabView
的部份。現在讓我們看看到 SplitView
吧!此時,TripView
及 PlaceView
的程式碼看起來像這樣:
struct TripsView: View {
var trips: [Trip]
var body: some View {
List {
ForEach(trips) { (trip) in
Text(trip.id)
}
}
}
}
struct PlacesView: View {
var places: [Place]
var body: some View {
List {
ForEach(places) { (place) in
Text(place.id)
}
}
}
}
如你所見,TripView
及 PlaceView
都是由一個簡單列表組成,從中建立 trips
裡每個元素的實例。假設我們希望能夠看到每個 trip 的細項的話,就需要將 List
包進一個 NavigationView
裡,而且列表裡的每個項目也需要包入在 NavigationLink
裡,像這樣:
struct TripsView: View {
var trips: [Trip]
var body: some View {
NavigationView {
List {
ForEach(trips) { (trip) in
NavigationLink(destination: TripDetails(trip:trip)) {
Text(trip.id)
}
}
}.navigationBarTitle(Text("Trips"))
}
}
}
要注意的是 NavigationBarTitle
修飾符是在 List
的尾端,而不是 NavigationView
的尾端。接著,啟動 App,現在你會看到顯示存在連結的按鈕:

點擊其中一個,就會顯示一個 TripDetails
視圖:

點下返回按鈕,你就會回到 Trip 列表。
範例 App 在 iPad 上的問題
到目前為止,我只使用 iPhone 模擬器來示範。如果使用 iPad 模擬器的話,會是這樣的結果:

我想你也看得出問題所在:點擊 Places Tab 後出現的竟是一個空白畫面,這到底是為甚麼呢?當我第一次碰到這個問題時,我以為是某個步驟出錯了,花了我不少的時間才弄清楚原因,原來是 SwiftUI 使用了一個 SplitView!所以我只需要從左滑向右,一個有 Trip 列表的 MasterView 就這樣出現了。點擊其中一個項目,就會顯示在 MasterView 後面的 DetailView。如果你點擊 DetailView,它會蓋過 MasterView。但因為沒有返回按鈕,所以你必須再次向右滑動才能回到上頁。

這雖然是可以正確運作,但糟糕的介面設計讓我完全不想釋出這樣的 App。在網路上做了很多資料搜集後,我終於找到解答!所有答案都在文件裡,只是不容易發現。所以,我要做的就只是添加 .navigationViewStyle(StackNavigationViewStyle()
到 NavigationView
裡,這麼一來 App 就會像 iPhone 版本一樣運作,MasterView 會填滿整個視窗畫面,並且當列表上的項目被點選時,DetailView 會滑入進來。
雖然 iPad 的 MasterView 和 DetailView 架構清楚,但我還沒有找到方法來克服這個使用上的問題。不過,我肯定 Apple 很快會找出方法來複製 UISplitView
的 UIKit 版本;但目前來說,我可以接受上面提到的解決方法。
本次教學就到這裡,希望這篇文章可以對使用 SwiftUI 作業的各位有所幫助。