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 中文版
iOS 18 新API:使用 Navigation Transition 創建 Hero 動畫式過場
SwiftUI 框架

iOS 18 新API:使用 Navigation Transition 創建 Hero 動畫式過場

Apple 的工程師可能早已認識到,許多 iOS 開發者都希望能夠重現 App Store 應用程式中的優雅 Hero 動畫。由於從頭實現這種動畫通常需要耗費大量時間與精力,Apple 在 iOS 18 SDK 中納入了這項功能。 透過這次更新,你現在只需少量的程式碼就能在自己的應用程式中實現類似的動畫過渡效果。這項重大改進讓開發者能夠創造出更具視覺吸引力且流暢的過渡效果,
如何使用 Vision APIs 從圖像中辨識文字
AI

如何使用 Vision APIs 從圖像中辨識文字

Vision 框架長期以來一直包含文字識別功能。我們已經有詳細的教程,向你展示如何使用 Vision 框架掃描圖像並執行文字識別。之前,我們使用了 VNImageRequestHandler 和 VNRecognizeTextRequest 來從圖像中提取文字。 多年來,Vision 框架已經顯著演變。在 iOS 18 中,Vision
iOS 18更新:SwiftUI 新功能介紹
SwiftUI 框架

iOS 18更新:SwiftUI 新功能介紹

SwiftUI的技術不斷演進,每次更新都讓 iOS 應用程式開發變得更加便捷。隨著 iOS 18 Beta 的推出,SwiftUI 引入了多個令人興奮的新功能,使開發者僅需幾行程式碼即可實現出色的效果。 本教學文章旨在探索這個版本中的幾項主要改進,幫助你了解如何運用這些新功能。 浮動標籤列 (Floating Tab Bar)SwiftUI中的標籤視圖(Tab
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。