利用 matchedGeometryEffect 輕鬆實作動畫導航選單


我很喜歡使用 SwiftUI 框架進行編程,其中一個原因就是它讓我們可以輕鬆為視圖變化設置動畫。iOS 14 中引入的 matchedGeometryEffect 修飾符 (modifier),進一步簡化了實作視圖動畫的步驟。有了這個修飾符,我們只需要描述兩個視圖的外觀,修飾符就會計算兩個視圖之間的差異,並自動為其大小/位置變化設置動畫。

我們之前就寫過一篇關於 matchedGeometryEffect詳細教學。如果你還沒有接觸過這個修飾符,我建議你可以先閱讀那篇教學。在這篇文章中,我們會利用 matchedGeometryEffect 開發一個這樣的動畫導航選單 (navigation menu):

swiftui-animated-navigation-menu-matchedGeometryEffect

編者備註:如果你想深入了解 SwiftUI 動畫和 SwiftUI 框架,可以參考這本書

建立導航選單

在建立動畫選單之前,我們要先建立一個靜止的版本。在我們的範例中,導航選單只有 3 個項目。

navigation-menu-swiftui

為了平均地水平佈局 (layout) 3 個文本視圖 (text view),我們會使用 HStack 視圖和 Spacer 來排列視圖。以下是範例程式碼:

struct NavigationMenu: View {

    let menuItems = [ "Travel", "Nature", "Architecture" ]

    var body: some View {
        HStack {
            Spacer()

            Text(menuItems[0])
                .padding(.horizontal)
                .padding(.vertical, 4)
                .background(Capsule().foregroundColor(Color.purple))
                .foregroundColor(.white)

            Spacer()

            Text(menuItems[1])
                .padding(.horizontal)
                .padding(.vertical, 4)
                .background(Capsule().foregroundColor(Color(uiColor: .systemGray5)))

            Spacer()

            Text(menuItems[2])
                .padding(.horizontal)
                .padding(.vertical, 4)
                .background(Capsule().foregroundColor(Color(uiColor: .systemGray5)))

            Spacer()
        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .padding()
    }
}

我們可以看到上面的程式碼有很多重複的地方,因此可以使用 ForEach 來簡化程式碼:

struct NavigationMenu: View {
    @State var selectedIndex = 0
    var menuItems = [ "Travel", "Nature", "Architecture" ]

    var body: some View {
        HStack {
            Spacer()

            ForEach(menuItems.indices) { index in

                if index == selectedIndex {
                    Text(menuItems[index])
                        .padding(.horizontal)
                        .padding(.vertical, 4)
                        .background(Capsule().foregroundColor(Color.purple))
                        .foregroundColor(.white)
                } else {
                    Text(menuItems[index])
                        .padding(.horizontal)
                        .padding(.vertical, 4)
                        .background(Capsule().foregroundColor(Color(uiColor: .systemGray5)))
                        .onTapGesture {
                            selectedIndex = index
                        }
                }

                Spacer()
            }

        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .padding()
    }
}

我們添加了一個 selectedIndex 的狀態變數 (state variable),來追踪所選的選單項目。當某個選單項目被點選的時候,我們就會以紫色突顯 (highlight) 它;如果項目沒有被點選,其背景顏色就會是淺灰色。

我們把 .onTapGesture 修飾符附加到文本視圖,來偵測使用者的點擊。當視圖被點擊時,我們就會更新 selectedIndex 的數值,來突顯選定的文本視圖。

利用 matchedGeometryEffect 為導航選單設置動畫

實作好導航選單,就可以開始設置動畫了。要設置選單項目被點選的視圖更改動畫,我們只需要創建一個 namespace 變數,並將 matchedGeometryEffect 修飾符附加到紫色的文本視圖:

struct NavigationMenu: View {
    @Namespace private var menuItemTransition

    .
    .
    .

    var body: some View {
        HStack {
            Spacer()

            ForEach(menuItems.indices) { index in

                if index == selectedIndex {
                    Text(menuItems[index])
                        .padding(.horizontal)
                        .padding(.vertical, 4)
                        .background(Capsule().foregroundColor(Color.purple))
                        .foregroundColor(.white)
                        .matchedGeometryEffect(id: "menuItem", in: menuItemTransition)
                } else {
                    .
                    .
                    .
                }

                Spacer()
            }

        }
        .frame(minWidth: 0, maxWidth: .infinity)
        .padding()
        .animation(.easeInOut, value: selectedIndex)
    }
}

ID 和 namespace 用於標示哪些視圖屬於同一個過場 (transition)。我們還需要將 .animation 修飾符附加到 HStack 視圖,以啟用視圖動畫。請注意,這個專案是用 Xcode 13 構建的,animation 修飾符在新版本的 iOS 中有所更新。我們必須提供一個監察變化的數值,在這裡,就是 selectedIndex

進行更改後,我們可以在模擬器中測試 NavigationMenu 視圖。隨意點擊一個選單選項,你就會看到選項轉換的漂亮動畫。

animated-swiftui-navigation-menu

使用動畫導航選單視圖

要在專案中使用這個動畫導航選單,我們可以修改 NavigationMenu 視圖,以接受與 selectedIndex 的綁定 (binding):

@Binding var selectedIndex: Int

舉個例子,我們建立了一個這樣的 page-based tab 視圖:

struct ContentView: View {
    @State var selectedTabIndex = 0
    let menuItems = [ "Travel", "Film", "Food & Drink" ]

    var body: some View {
        TabView(selection: $selectedTabIndex) {

            ForEach(menuItems.indices) { index in
                Text(menuItems[index])
                    .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
                    .background(Color.green)
                    .foregroundColor(.white)
                    .font(.system(size: 50, weight: .heavy, design: .rounded))
                    .tag(index)
            }
        }
        .tabViewStyle(.page(indexDisplayMode: .never))
        .ignoresSafeArea()
        .overlay(alignment: .bottom) {
            NavigationMenu(selectedIndex: $selectedTabIndex, menuItems: menuItems)
        }
    }
}

我們就可以把 NavigationMenu 視圖添加為 overlay,並使用自己的選單選項。

swiftui-tab-view-animated-tab-bar

譯者簡介:Kelly Chan-AppCoda 編輯小姐。
原文How to Create an Animated Navigation Menu in SwiftUI Using matchedGeometryEffect


軟件工程師,AppCoda 創辦人。著有《iOS 13 App 程式設計實力超進化實戰攻略》、《iOS 13 App 程式設計實力超進化實戰攻略》以及《精通 SwiftUI》。曾任職於HSBC, FedEx等公司,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda,致力於iOS程式教學,產品設計及開發。

blog comments powered by Disqus
Shares
Share This