你是否曾在 Keynote 使用過奇妙的動作動畫?有了這些奇妙的動作,你可以輕易的建立兩張投影片間的滑動動畫 (slick animation)。 Keynote 會自動地分析兩張投影片間的物件,然後自動地渲染動畫。同樣,SwiftUI 也將奇妙動作 (Magic Move) 動畫帶入了 App 的開發中。動畫所用的框架是自動且神奇的。你只要定義一個視圖的兩個狀態,SwiftUI 會自動進行計算,接著以動畫的方式來呈現狀態之間的變化。
SwiftUI 可以讓你針對個別視圖內的變化以動畫來呈現,也可以實作視圖之間的轉場 (transition) 動作。
在本教學文,你將學習如何使用 SwiftUI 所提供的隱式 (implicit) 與顯式 (explicit) 動畫。另外,我們會用幾個範例專案來學習這些程式技巧並建立下載指示器 (Loading Indicator)。
隱式與顯示動畫
SwiftUI 提供兩種動畫類型:隱式與顯式。這兩個方法可以產生視圖動畫與視圖間的轉場效果。這個框架提供一個稱作 animation
的框架來實作隱式動畫。把這個修飾器加到你想要呈現動畫的視圖上,並指定你所需要到動畫類型,另外,你可以定義動畫持續時間與延遲時間。SwiftUI 會依照視圖狀態的變化自動渲染動畫。
顯式動畫提供精緻的動畫控制讓你呈現想要的動畫效果。顯示動畫不是將修飾器貼到視圖上,而是在 withAnimation()
區塊內,告訴 SwiftUI ,什麼樣的狀態做改變時,你想要呈現什麼樣的動畫。
還是搞不清楚嗎?沒有關係,練習幾個範例你就會更有概念了。
隱式動畫
我們從隱式動畫來開始,我建議你建立一個新專案來實際看看動畫的動作。你可以任意為專案命名,我將它命名為 SwiftUIAnimation
。
以圖 1 來看,這個視圖很簡單,可以點擊,由紅色圓與心形組成。當一個使用者點擊心形或者圓,圓的顏色會變成淡灰色,心形則會變成紅色,同時間心形的也會變得大一點,因此,我們有幾個狀態會做變化:
- 圓的顏色從紅色變成淡灰色。
- 心形圖示從白色變成紅色。
- 心形圖示變成兩倍大。
如果你使用 SwiftUI 來實作可點擊的圓,程式內容如下所示:
struct ContentView: View {
@State private var circleColorChanged = false
@State private var heartColorChanged = false
@State private var heartSizeChanged = false
var body: some View {
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
Image(systemName: "heart.fill")
.foregroundColor(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}
}
}
我們定義了三個狀態變數來建立狀態模型,初始值設為 false。圓與心形則使用 ZStack
來建立,將心形疊加在圓形上面。 SwiftUI 有一個 onTapGesture
修飾器可以偵測手勢。你可以將它附加上任何視圖要做點擊的地方。在 onTapGesture
閉包中,我們使用 toggle() 來開啟狀態以改變視圖的外觀。
倘若你在畫布執行這個 App,當你點擊視圖時,這個圓與心形的圖示會做改變,不過這些變化沒有動畫。
要讓變化呈現動畫效果,你需要加上一個 animation
修飾器至 Circle
與 Image
視圖:
Circle()
.frame(width: 200, height: 200)
.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
.animation(.default)
Image(systemName: "heart.fill")
.foregroundColor(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
.animation(.default)
SwiftUI 自動計算與渲染動畫,讓視圖可以很流暢的從一個狀態轉換到另一個轉態。按下心形一次,你會見到一個滑動動畫。
你不止可以在一個單一視圖中應用 animation
修飾器,它也適用不同視圖的群組,舉例來說,你可以將 animation
修飾器加到 ZStack
,將以上的程式重新撰寫如下:
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
Image(systemName: "heart.fill")
.foregroundColor(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.animation(.default)
.onTapGesture {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}
執行結果是一樣的, SwiftUI 尋找嵌入在 ZStack
中所有的改變狀態,並建立動畫。
在範例中,我們使用預設動畫,SwiftUI 提供了幾個內建動畫供選擇,其中包括了 linear
、 easeIn
,、easeOut
、easeInOut
與 spring
。線性動畫 (linear
) 動畫是以線性速度來呈現變化,而緩動動畫 (ease easing animations) 則速度會做變化。細節部分你可以參考 www.easings.net 來了解每一個 ease 函數的不同之處。
要使用其他的動畫,你只需要在動畫修飾器中設定指定的動畫。譬如說,想要使用一個 spring
動畫,你可以將 .default
值變更如下:
.animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3))
這會渲染一個以彈性動畫 (spring animation),讓心形有一個心跳的特效。你可以調整阻尼 (damping) 與融合 (blend) 值來達到不同效果。
顯式動畫
以上是對視圖使用隱式動畫的方法。我們來看如何使用顯示動畫來達到同樣的結果。如之前所說明的,你可以將這些改變狀態包進去 withAnimation
區塊內。要建立同樣的效果,如以下程式所示:
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
Image(systemName: "heart.fill")
.foregroundColor(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
withAnimation(.default) {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}
}
我們不再使用 animation
修飾器,我們將程式以 withAnimation
包在 onTapGesture
中。呼叫這個 withAnimation
會帶入一個動畫參數,這裡我們指定使用預設動畫。
當然,你也可以像以下這樣更新 withAnimation
,將動畫變更為彈性動畫:
withAnimation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3)) {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}
有了顯式動畫,你可以很簡單的為任一個你想控制的狀態加上動畫。舉例來說,如果你不想要讓心形圖示的大小有所改變,你可以將該行程式從 withAnimation
排除,如下所示:
.onTapGesture {
withAnimation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3)) {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
}
self.heartSizeChanged.toggle()
}
在這個情況下,SwiftUI 只會針對圓形與心形進行顏色的變化,就看不到心形圖示變大的效果。
你可能想知道,是否可以使用隱式動畫來關掉縮放動畫。好的,一樣是可以辦到,你可以將 animation(nil)
加到視圖中來防止 SwiftUI 產生狀態變化時的動畫。以下為達到同樣效果的程式:
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundColor(circleColorChanged ? Color(.systemGray5) : .red)
.animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3))
Image(systemName: "heart.fill")
.foregroundColor(heartColorChanged ? .red : .white)
.font(.system(size: 100))
.animation(nil) // 從此處取消動畫
.scaleEffect(heartSizeChanged ? 1.0 : 0.5)
.animation(.spring(response: 0.3, dampingFraction: 0.3, blendDuration: 0.3))
}
.onTapGesture {
self.circleColorChanged.toggle()
self.heartColorChanged.toggle()
self.heartSizeChanged.toggle()
}
我們在 scaleEffect
之前插入 animation(nil)
修飾器。這會將動畫取消, scaleEffect
修飾器的狀態改變動畫將不會再發生。
雖然你可以使用隱式動畫建立同樣的動畫,我認為在這種情況下使用顯式動畫會更為方便。
使用 RotationEffect 建立一個下載指示器
SwiftUI 的動畫的威力是你不需要去了解這些視圖動畫是如何產生,你只需要提供起始與結束狀態。SwiftUI 會幫忙解決後續的工作。如果你具備了這個觀念,你便可以建立各式不同類型的動畫。
舉例來說,我們建立一個簡單的下載指示器,這個指示器在市面上的應用很常見,像是 Medium ,要建立一個如上圖的指示器,我們一開始建立一個開口圓,程式如下所示:
Circle()
.trim(from: 0, to: 0.7)
.stroke(Color.green, lineWidth: 5)
.frame(width: 100, height: 100)
那麼,我們要如何讓圓能夠持續旋轉呢?我們可以利用 rotationEffect
與 animation
修飾器。技巧就是在於讓圓能夠持續以 360 度來旋轉。以下為其程式碼:
struct ContentView: View {
@State private var isLoading = false
var body: some View {
Circle()
.trim(from: 0, to: 0.7)
.stroke(Color.green, lineWidth: 5)
.frame(width: 100, height: 100)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(Animation.default.repeatForever(autoreverses: false))
.onAppear() {
self.isLoading = true
}
}
}
rotationEffect
修飾器有旋轉角度的參數設定,在上面的程式中,我們有一個狀態變數作為控制下載狀態用。當它設定為 true 時,這個旋轉角度將會以 360 度來轉動圓。在 animation
修飾器,我們指定使用預設動畫,不過還有些不同,我們告訴 SwiftUI 要一遍又一遍重複同樣的動畫,這是建立下載動畫的關鍵部分。
如果你想要變更動畫的速度,你可以使用線性動畫,並指定持續時間,如下:
Animation.linear(duration: 1).repeatForever(autoreverses: false)
值設的越大,則動畫越慢。
這裡的 onAppear
修飾器對你來說可能比較陌生,如果你對 UIKit 有些理解的話,這個修飾器跟 viewDidAppear
非常相似。當視圖出現在畫面時會自動呼叫。在這個程式中,當視圖載入時,為了啟動這個動畫,我們變更下載狀態為 true。
一旦你熟悉了這個技術,你可以調整設計並開發各種不同版本的下載指示器。譬如說,你可以疊加一個圓弧在圓圈上面來建立酷炫的指示器。
程式碼如下所示:
struct ContentView: View {
@State private var isLoading = false
var body: some View {
ZStack {
Circle()
.stroke(Color(.systemGray5), lineWidth: 14)
.frame(width: 100, height: 100)
Circle()
.trim(from: 0, to: 0.2)
.stroke(Color.green, lineWidth: 7)
.frame(width: 100, height: 100)
.rotationEffect(Angle(degrees: isLoading ? 360 : 0))
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false))
.onAppear() {
self.isLoading = true
}
}
}
}
這個下載指示器,不一定是要圓形,你也可以使用 Rectangle
或 RoundedRectangle
來建立指示器。不過這裡不變更旋轉角度,你可以修改偏移 (offset) 值來建立如下圖的動畫。
我們將兩個圓角矩形疊在一起來建立這個動畫。上面的矩形比下面的短,當載入開始時,我們將 offset 值從 -110 更新為 110 。
struct ContentView: View {
@State private var isLoading = false
var body: some View {
ZStack {
Text("Loading")
.font(.system(.body, design: .rounded))
.bold()
.offset(x: 0, y: -25)
RoundedRectangle(cornerRadius: 3)
.stroke(Color(.systemGray5), lineWidth: 3)
.frame(width: 250, height: 3)
RoundedRectangle(cornerRadius: 3)
.stroke(Color.green, lineWidth: 3)
.frame(width: 30, height: 3)
.offset(x: isLoading ? 110 : -110, y: 0)
.animation(Animation.linear(duration: 1).repeatForever(autoreverses: false))
}
.onAppear() {
self.isLoading = true
}
}
}
這會讓上面的矩形沿著線移動。另外,當你一遍又一遍重複同樣的動畫,它會變成一個載入動畫,下圖為偏移植的說明。
總結
即使有經驗的開發者,要處理一個滑動動畫也不是一件容易的事。很幸運地,SwiftUI 框架簡化了 UI 動畫與轉場開發過程。你只要告訴框架視圖在開始與結束該怎麼做即可。SwiftUI 會處理剩下的任務,為你渲染出一個流暢且漂亮的動畫。
在此教學文,我已經介紹了基本的原理,不過如你所見,你已經建立了一些漂亮的動畫與轉場效果。更重要的是,只要幾行程式即能辦到。
原文: SwiftUI Animation Basics: Building a Loading Indicator