我很喜歡使用 SwiftUI 框架進行編程,其中一個原因就是它讓我們可以輕鬆為視圖變化設置動畫。iOS 14 中引入的 matchedGeometryEffect
修飾符 (modifier),進一步簡化了實作視圖動畫的步驟。有了這個修飾符,我們只需要描述兩個視圖的外觀,修飾符就會計算兩個視圖之間的差異,並自動為其大小/位置變化設置動畫。
我們之前就寫過一篇關於 matchedGeometryEffect
的詳細教學。如果你還沒有接觸過這個修飾符,我建議你可以先閱讀那篇教學。在這篇文章中,我們會利用 matchedGeometryEffect
開發一個這樣的動畫導航選單 (navigation menu):
編者備註:如果你想深入了解 SwiftUI 動畫和 SwiftUI 框架,可以參考這本書。
建立導航選單
在建立動畫選單之前,我們要先建立一個靜止的版本。在我們的範例中,導航選單只有 3 個項目。
為了平均地水平佈局 (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
視圖。隨意點擊一個選單選項,你就會看到選項轉換的漂亮動畫。
使用動畫導航選單視圖
要在專案中使用這個動畫導航選單,我們可以修改 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,並使用自己的選單選項。
原文:How to Create an Animated Navigation Menu in SwiftUI Using matchedGeometryEffect