無論是在構建社交媒體 App 或生產力工具 (productivity tool) 時,我們都可以利用標籤列 (Tab Bar) 介面讓它更加直觀和易於使用,提升使用者體驗。現在有了 SwiftUI 的 TabView
,要創建無縫 (seamless)、而且可以客製化的標籤介面 (tab interface) 十分簡單。
在預設情況下,iOS 會以標準形式顯示標籤列,方便使用者可以快速切換不同 App。但是,開發者可能會想客製化標籤列,來滿足 App 的特定需求。
在這篇教學文章中,我會帶大家利用 SwiftUI 構建一個可滾動的動畫標籤列,並支援無限的標籤項目 (tab item)。完成這篇教學之後,我們會實作出以下的標籤列:
標籤視圖 (Tab View) 和標籤列簡介
如果你還沒有用過 TabView
,可以先看看以下的簡介。要創建一個標籤視圖,我們只需要使用 TabView
,並在當中嵌入子視圖。我們可以應用 tabItem
修飾符,指定每個子視圖的項目描述。讓我們看看以下例子:
struct ContentView: View {
let colors: [Color] = [ .yellow, .blue, .green, .indigo, .brown ]
let tabbarItems = [ "Random", "Travel", "Wallpaper", "Food", "Interior Design" ]
var body: some View {
TabView {
ForEach(colors.indices, id: \.self) { index in
colors[index]
.frame(maxWidth: .infinity, maxHeight: .infinity)
.tag(index)
.tabItem {
Image(systemName: "\(index + 1).circle")
Text(tabbarItems[index])
}
}
}
}
}
以上的程式碼會創建出一個簡單的標籤視圖,當中有 5 個標籤項目。我們用了 Image
來顯示標籤圖示 (icon)。如果我們在 Xcode 編寫以上的程式碼,應該會在預覽中看到以下的標籤列:
TabView
有另一個 init
方法,它需要一個狀態變量,當中包含標籤的 tag value。
TabView(selection: $selectedIndex)
舉個例子,讓我們在 ContentView
內宣告以下狀態變數:
@State private var selectedIndex = 0
現在,如果我們改變 selectedIndex
的數值,標籤視圖就會自動轉換到相應的標籤。我們可以這樣更改程式碼來測試一下:
TabView(selection: $selectedIndex) {
.
.
.
}
.onAppear {
selectedIndex = 2
}
你會發現,在顯示標籤視圖時,會自動選擇第三個標籤。
建立一個客製化的可滾動標籤列
如上圖的範例結果可見,我們想構建的標籤列是可滾動的,如果我們想放多於 5 個項目到標籤列,這個功能就十分有用了。要建立這個客製化標籤列,我們會用 ScrollView
和 ScrollViewReader
來構建自己的視圖。
讓我們這樣構建標籤列視圖,並把它命名為 TabBarView
:
struct TabBarView: View {
var tabbarItems: [String]
@State var selectedIndex = 0
var body: some View {
ScrollViewReader { scrollView in
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(tabbarItems.indices, id: \.self) { index in
Text(tabbarItems[index])
.font(.subheadline)
.padding(.horizontal)
.padding(.vertical, 4)
.foregroundColor(selectedIndex == index ? .white : .black)
.background(Capsule().foregroundColor(selectedIndex == index ? .purple : .clear))
.onTapGesture {
withAnimation(.easeInOut) {
selectedIndex = index
}
}
}
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(25)
}
}
}
這個客製化標籤視圖可以接受一個陣列的標籤列項目。在這個範例中,我們會使用 String
陣列。但在實際 App 中,大家可以為選擇自己的客製化型別。
為了在標籤列中啟用滾動功能,我們會把所有標籤項目嵌入到一個滾動視圖中。此外,我們會用 ScrollViewReader 包裝滾動視圖,以確保所選的標籤項目是可見的。
在選擇特定標籤項目時,我們更新了 selectedIndex
變數來反映所選的 index。如此一來,我們就可以 highlight 所選的標籤項目,並向使用者提供反饋。
我們可以在預覽添加 TabBarView
,來預覽這個客製化的標籤列。
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
TabBarView(tabbarItems: [ "Random", "Travel", "Wallpaper", "Food", "Interior Design" ]).previewDisplayName("TabBarView")
}
}
現在,這個客製化的標籤列可以正常操作。但是,我們需要手動滾動標籤列,才能顯示最後一個項目。要解決這個問題,我們可以把以下程式碼添加到 ScrollView
:
.onChange(of: selectedIndex) { index in
withAnimation {
scrollView.scrollTo(index, anchor: .center)
}
}
當所選 index 更新後,我們會呼叫 scrollTo
方法來移動滾動視圖。
利用 matchedGeometryEffect 建立更漂亮的動畫
我們建立了一個動態、而且可滾動的標籤列,但我們其實可以建立更漂亮的動畫。現在,在切換標籤項目時,標籤列使用「淡出」動畫。如果我們在標籤列搭配使用 matchedGeometryEffect
,就可以創建更流暢更漂亮的動畫。讓我們看看如何實作吧!
首先,為標籤列項目建立一個新結構 TabbarItem
:
struct TabbarItem: View {
var name: String
var isActive: Bool = false
let namespace: Namespace.ID
var body: some View {
if isActive {
Text(name)
.font(.subheadline)
.padding(.horizontal)
.padding(.vertical, 4)
.foregroundColor(.white)
.background(Capsule().foregroundColor(.purple))
.matchedGeometryEffect(id: "highlightmenuitem", in: namespace)
} else {
Text(name)
.font(.subheadline)
.padding(.horizontal)
.padding(.vertical, 4)
.foregroundColor(.black)
}
}
}
有了 matchedGeometryEffect
,我們只需要描述兩個視圖的外觀即可。然後,修飾符就會計算兩個視圖之間的差異,並自動為大小或位置變化設置動畫。因此,在上面的程式碼中,我們把被選擇的標籤項目 highlight 為紫色,而沒被選擇的項目則以普通文本樣式顯示。
在 TabBarView
中,宣告一個新的 namespace 變數:
@Namespace private var menuItemTransition
然後,這樣重新編寫 ForEach
loop 的程式碼:
ForEach(tabbarItems.indices, id: \.self) { index in
TabbarItem(name: tabbarItems[index], isActive: selectedIndex == index, namespace: menuItemTransition)
.onTapGesture {
withAnimation(.easeInOut) {
selectedIndex = index
}
}
}
更新程式碼後,你會發現切換標籤項目的動畫變得更流暢和漂亮了。
使用客製化標籤列
在把 TabBarView
應用到 ContentView
之前,我們需要先在 TabBarView
做一個小改動。在 TabBarView
中,這樣把狀態變數修改為綁定變數:
@Binding var selectedIndex: Int
現在,我們就可以把這個客製化標籤列應用到其他視圖了。在 ContentView
這樣更新 body
:
ZStack(alignment: .bottom) {
TabView(selection: $selectedIndex) {
ForEach(colors.indices, id: \.self) { index in
colors[index]
.frame(maxWidth: .infinity, maxHeight: .infinity)
.tag(index)
.ignoresSafeArea()
}
}
.ignoresSafeArea()
TabBarView(tabbarItems: tabbarItems, selectedIndex: $selectedIndex)
.padding(.horizontal)
}
要把客製化標籤列合併到 App 中十分簡單,我們只需要把 TabView
包裝在 ZStack
中,並在上面疊加 TabBarView
,就可以輕鬆地把將標籤列合併到 tab UI。
我們還需要更新預覽結構,來讓專案順利執行:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
TabBarView(tabbarItems: [ "Random", "Travel", "Wallpaper", "Food", "Interior Design" ], selectedIndex: .constant(0)).previewDisplayName("TabBarView")
}
}
現在讓我們測試一下標籤列的 UI 吧:
總結
對很多流行的手機 App 來說,標籤列介面是非常重要的元素,讓使用者可以快速和方便地切換到 App 的不同功能。雖然在大多數情況下,標準的標籤列都已經可以滿足我們的要求,但有時我們還是會希望客製化標籤列,以提升使用者體驗。
在這篇教學文章中,我們構建了一個動態、而且可滾動標籤列,並支援無限的標籤項目。我們還可以搭配 matchedGeometryEffect
使用,把標籤列的動畫提升到另一個層次。學會了這篇文章的技巧後,你就可以按自己 App 的需要,設計出無縫而且直觀的客製化標籤列。
如果大家想更深入了解 SwiftUI,可以參閱我們的《精通SwiftUI》一書。