SwiftUI 框架

在 SwiftUI 構建可滾動的客製化標籤列 大大提升使用者體驗

很多流行的手機 App 都用到 Tab Bar,讓使用者可以快速和方便地切換到 App 的不同功能,大大提升使用者體驗。在這篇文章中,我會帶大家使用 SwiftUI 的 TabView,輕鬆地客製化一個可滾動的 Tab Bar,並添加漂亮的動畫,來滿足你的 App 的需要。
在 SwiftUI 構建可滾動的客製化標籤列 大大提升使用者體驗
Photo by Erik Mclean on Unsplash
在 SwiftUI 構建可滾動的客製化標籤列 大大提升使用者體驗
Photo by Erik Mclean on Unsplash
In: SwiftUI 框架

無論是在構建社交媒體 App 或生產力工具 (productivity tool) 時,我們都可以利用標籤列 (Tab Bar) 介面讓它更加直觀和易於使用,提升使用者體驗。現在有了 SwiftUI 的 TabView,要創建無縫 (seamless)、而且可以客製化的標籤介面 (tab interface) 十分簡單。

在預設情況下,iOS 會以標準形式顯示標籤列,方便使用者可以快速切換不同 App。但是,開發者可能會想客製化標籤列,來滿足 App 的特定需求。

在這篇教學文章中,我會帶大家利用 SwiftUI 構建一個可滾動的動畫標籤列,並支援無限的標籤項目 (tab item)。完成這篇教學之後,我們會實作出以下的標籤列:

swiftui-tabbar-final

標籤視圖 (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 編寫以上的程式碼,應該會在預覽中看到以下的標籤列:

Sample tab bar using SwiftUI

TabView 有另一個 init  方法,它需要一個狀態變量,當中包含標籤的 tag value。

TabView(selection: $selectedIndex)

舉個例子,讓我們在 ContentView 內宣告以下狀態變數:

@State private var selectedIndex = 0

現在,如果我們改變 selectedIndex 的數值,標籤視圖就會自動轉換到相應的標籤。我們可以這樣更改程式碼來測試一下:

TabView(selection: $selectedIndex) {
   .
   .
   .
}
.onAppear {
    selectedIndex = 2
}

你會發現,在顯示標籤視圖時,會自動選擇第三個標籤。

建立一個客製化的可滾動標籤列

swiftui-animated-custom-tab-bar

如上圖的範例結果可見,我們想構建的標籤列是可滾動的,如果我們想放多於 5 個項目到標籤列,這個功能就十分有用了。要建立這個客製化標籤列,我們會用  ScrollViewScrollViewReader 來構建自己的視圖。

讓我們這樣構建標籤列視圖,並把它命名為 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 所選的標籤項目,並向使用者提供反饋。

manually-scrollable-tab-bar

我們可以在預覽添加 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
            }
        }
}

更新程式碼後,你會發現切換標籤項目的動畫變得更流暢和漂亮了。

swiftui-matchedgeometryeffect-tab-bar

使用客製化標籤列

在把 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 吧:

animated-scrollable-tab-bar-swiftui

總結

對很多流行的手機 App 來說,標籤列介面是非常重要的元素,讓使用者可以快速和方便地切換到 App 的不同功能。雖然在大多數情況下,標準的標籤列都已經可以滿足我們的要求,但有時我們還是會希望客製化標籤列,以提升使用者體驗。

在這篇教學文章中,我們構建了一個動態、而且可滾動標籤列,並支援無限的標籤項目。我們還可以搭配 matchedGeometryEffect 使用,把標籤列的動畫提升到另一個層次。學會了這篇文章的技巧後,你就可以按自己 App 的需要,設計出無縫而且直觀的客製化標籤列。

如果大家想更深入了解 SwiftUI,可以參閱我們的《精通SwiftUI》一書。

譯者簡介:Kelly Chan-AppCoda 編輯小姐。
作者
Simon Ng
軟體工程師,AppCoda 創辦人。著有《iOS 17 App 程式設計實戰心法》、《iOS 17 App程式設計進階攻略》以及《精通SwiftUI》。曾任職於HSBC, FedEx等跨國企業,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda業務,致力於iOS程式教學、產品設計及開發。你可以到推特與我聯絡。
評論
更多來自 AppCoda 中文版
如何使用 Swift 整合 Google Gemini AI
SwiftUI 框架

如何使用 Swift 整合 Google Gemini AI

在即將到來的 WWDC,Apple 預計將會發佈一個本地端的大型語言模型 (LLM)。 接下來的 iOS SDK 版本將讓開發者更輕易地整合 AI 功能至他們的應用程式中。然而,當我們正在等待 Apple 推出自家的生成 AI 模型時,其他公司(如 OpenAI
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。