SwiftUI 有趣的 animation 動畫

透過 SwiftUI,我們可以快速實現 App 的動畫功能,接下來就讓我們以奔跑吧,Peter 為例,說明各種有趣的動畫效果。

設計 App 畫面

struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1

var body: some View {
VStack {
Button("奔跑吧,Peter") {
moveDistance += 100
opacity -= 0.3
}
.font(.title)
HStack {
Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
Spacer()
}
}

}
}

當我們點擊 奔跑吧,Peter 時,peter 的圖片將向右移動 100 points,而且因為跑步很累,他將會愈來愈透明。

不過剛剛的 peter 是瞬間移動,這樣太不真實了,讓我們為他加個移動的動畫吧。

從元件呼叫 function animation 加入動畫

我們想要展現動畫的目標是 Image,所以很簡單,我們只要從 Image 呼叫動畫 function animation 即可。

struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1

var body: some View {
VStack {
Button("奔跑吧,Peter") {
moveDistance += 100
opacity -= 0.3
}
.font(.title)
HStack {
Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
.animation(.default, value: opacity)
.animation(.default, value: moveDistance)
Spacer()
}
}

}
}

function animation 的宣告如下

func animation<V>(_ animation: Animation?, value: V) -> some View where V : Equatable

參數 animation 控制動畫的效果,在此我們傳入 .default 產生預設的動畫效果。value 則代表觸發動畫的內容,當 value 改變時將觸發動畫。在剛剛的程式我們呼叫兩次 animation,value 分別傳入 opacity & moveDistance,因此 opacity & moveDistance 改變時都會觸發動畫,造成點選 button 時圖片以動畫移動和改變透明度。

接著讓我們看一個有問題的例子。以下程式將不會產生動畫,因為 animation 的參數 value 傳入 opacity,但是點選 button 時只有改變 moveDistance,並沒有改變 opacity,因此不會觸發動畫。

struct ContentView: View {

@State private var moveDistance: Double = 0
@State private var opacity: Double = 1

var body: some View {

VStack {
Button("奔跑吧,Peter") {
moveDistance += 100
}
.font(.title)
HStack {
Image("peter")
.offset(x: moveDistance)
.opacity(opacity)
.animation(.default, value: opacity)
Spacer()
}
}
}
}

指定不同的動畫效果

剛剛的 .default 採用預設的動畫效果,身為創意無限的 iOS App 魔法師,我們常想做一些客製的動畫效果。

想做客製的動畫當然 ok 呀,剛剛從 Image 呼叫 function animation 時,我們可傳入 Animation 型別的東西指定動畫效果。從下圖的自動完成選單,我們看到可選擇的 Animation 超多的。

以下是幾種常見的 Animation 介紹

  • easeOut

動畫的速度一開始快,然後愈來愈慢。

Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
.animation(.easeOut, value: moveDistance)
.animation(.easeOut, value: opacity)
Apple 官方的 easeOut 動畫速度變化圖
  • easeIn

動畫速度一開始慢,然後愈來愈快。

.animation(.easeIn, value: moveDistance)
.animation(.easeIn, value: opacity)
Apple 官方的 easeIn 動畫速度變化圖
  • easeInOut

動畫速度一開始慢,然後愈來愈快,最後再愈來愈慢。

.animation(.easeInOut, value: moveDistance)
.animation(.easeInOut, value: opacity)
Apple 官方的 easeInOut 動畫速度變化圖
  • linear

動畫維持固定的速度。

.animation(.linear, value: moveDistance)
.animation(.linear, value: opacity)
Apple 官方的 linear 動畫速度變化圖
  • spring,smooth,snappy,bouncy

類似彈簧的動畫效果 ,它將像彈簧來回振動般變成最後的畫面,比方從座標 10 移動到 100,它將先移動到超過 100,然後再振動回小於 100,然後不斷來回振動直到抵達 100 的位置。

新版寫法。

參數 bounce 控制振動的次數,它的範圍是 0 ~ 1,數字愈大將振動愈多次。

.animation(.spring(bounce: 0.9), value: moveDistance)
.animation(.spring(bounce: 0.9), value: opacity)

SwiftUI 也提供一些預先定義好的 spring 動畫,振盪的次數由小到大依序為 smooth、snappy、bouncy。

.animation(.bouncy, value: moveDistance)
.animation(.bouncy, value: opacity)

舊版寫法。

spring 的參數 dampingFraction 控制振動的次數,它的範圍是 0 ~ 1,數字愈小將振動愈多次。

.animation(.spring(dampingFraction: 0.1), value: moveDistance)
.animation(.spring(dampingFraction: 0.1), value: opacity)
  • default

從 iOS 17 開始,default 的效果是 spring,在 iOS 17 之前則是 easeInOut。

.animation(.default, value: moveDistance)
.animation(.default, value: opacity)

調整動畫的時間

我們可透過兩種方法調整動畫的時間。

  • 產生 Animation 時傳入參數 duration

spring、smooth、snappy、bouncy、easeIn、easeOut、easeInOut、linear 都可以搭配參數 duration,單位為秒數,比方指定動畫的時間為 10 秒鐘。

Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
.animation(.easeOut(duration: 10), value: moveDistance)
.animation(.easeOut(duration: 10), value: opacity)
  • 呼叫 Animation 的 function speed

speed(0.5) 將讓速度變慢,變成 0.5 倍,因此時間變成原來的 2 倍。相反的,speed(2) 將讓速度加快,變成 2 倍,因此時間變成原本的 0.5 倍。

.animation(.linear.speed(0.5), value: moveDistance)
.animation(.linear.speed(0.5), value: opacity)

設定動畫重覆次數的 repeatCount

透過呼叫 Animation 的 function repeatCount。

struct ContentView: View {
@State private var rotateDegree: Double = 0

var body: some View {
VStack {
Button("轉吧,Peter") {
rotateDegree = 180
}
.font(.title)
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatCount(3, autoreverses: false),
value: rotateDegree
)
}

}
}

function repeatCount 的宣告以下,參數 repeatCount 控制動畫重覆的次數,autoreverses 控制是否會以動畫反轉回原來狀態再繼續下一次的動畫。

public func repeatCount(_ repeatCount: Int, autoreverses: Bool = true) -> Animation

如下圖所示,當我們呼叫 animation(.linear(duration: 5).repeatCount(3, autoreverses: false)) 時,圖片 Peter 將花 5 秒旋轉 180 度,然後再瞬間轉回 0 度,接著繼續 5 秒轉 180 度,然後再瞬間轉回 0 度,最後再做一次 5 秒轉 180 度,完成我們要求的 3 次動畫。

當參數 autoreverses 傳入 true 時,將有完全不同的效果。此時圖片 Peter 將花 5 秒旋轉 180 度,接著再花 5 秒轉回一開始的 0 度,最後再花 5 秒旋轉 180 度。由於回到原來狀態也算一次動畫,所以這樣已經算完成 3 次的動畫。

Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatCount(3, autoreverses: true),
value: rotateDegree
)

了解 repeatCount 的原理後,我們將可輕易實現 App 常見的轉圈動畫,以下程式在按下按鈕時將 rotateDegree 設為 360,讓 peter 轉了華麗的 3 圈。

struct ContentView: View {
@State private var rotateDegree: Double = 0

var body: some View {
VStack {
Button("轉吧,Peter") {
rotateDegree = 360
}
.font(.title)
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatCount(3, autoreverses: false),
value: rotateDegree
)
}

}
}

記得動畫的速度要設為 linear,旋轉才會順暢。若是設定其它動畫,比方 easeInOut,將發現圖片似乎體力不太好,轉完一圈後會停頓一下下再轉下一圈。

無限重覆動畫的 repeatForever,以無止盡的旋轉為例

有些特別的動畫我們會希望它永遠不要停止,就像我們希望好看的影集 the big bang theory 永遠不要完結篇一樣。

可惜現實世界的影集總有結束的一天,但在 SwiftUI 的世界,我們可以呼叫 repeatForever 讓動畫永不停止。

以下程式將讓 peter 無止盡地旋轉。

struct ContentView: View {
@State private var rotateDegree: Double = 0

var body: some View {
VStack {
Button("轉吧,Peter") {
rotateDegree = 360
}
.font(.title)
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatForever(autoreverses: false),
value: rotateDegree
)
}

}
}

畫面出現時開始動畫的 onAppear

我們可以在 onAppear 裡觸發動畫,讓動畫在畫面出現時自動開始表演。以下例子我們將 rotateDegree 設為 360 度,圖片將在畫面出現時自動開始旋轉。

struct ContentView: View {
@State private var rotateDegree: Double = 0

var body: some View {
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatForever(autoreverses: false),
value: rotateDegree
)
.onAppear {
rotateDegree = 360
}
}
}

延遲動畫的 delay

呼叫 Animation 的 delay function,讓動畫幾秒後再開始。

struct ContentView: View {
@State private var rotateDegree: Double = 0

var body: some View {
Image(.peter)
.rotationEffect(.degrees(rotateDegree))
.animation(
.linear(duration: 5)
.repeatForever(autoreverses: false)
.delay(5),
value: rotateDegree
)
.onAppear {
rotateDegree = 360
}
}
}

實現動畫的三種方法

SwiftUI 實現動畫的方法不只一種,以下三種方法都可以實現動畫。接下來我們再來認識一下其它兩種方法。

  • 從元件呼叫 function animation 加入動畫。
  • 利用 function withAnimation 加入動畫。
  • 將 animation 加在 Binding 變數上。

方法2: 利用 function withAnimation 加入動畫

在 withAnimation 的參數 body 裡傳入 closure,描述我們想要的改變, SwifUI 將聰明地以動畫呈現畫面的改變。

struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1

var body: some View {
VStack {
Button("奔跑吧,Peter") {
withAnimation {
moveDistance += 100
opacity -= 0.3
}
}
.font(.title)
HStack {
Image(.peter)
.offset(x: moveDistance)
.opacity(opacity)
Spacer()
}
}
}
}

function withAnimation 的宣告以下,剛剛的程式我們省略第一個參數 animation,因此它將採用預設的動畫效果。

public func withAnimation<Result>(_ animation: Animation? = .default, _ body: () throws -> Result) rethrows -> Result

呼叫 withAnimation 時,我們也可以在參數 animation 傳入想要的 Animation。

Button("奔跑吧,Peter") {
withAnimation(.easeInOut(duration: 10)) {
moveDistance += 100
opacity -= 0.3
}
}

方法3: 將 animation 加在 Binding 變數上

我們可以從 Binding 型別的變數呼叫 animation,當它的內容改變時,畫面的更新將以動畫呈現。

比方我們將 Button 包裝成 RunButton,在裡面以 @Binding 宣告 moveDistance & opacity。

struct RunButton: View {
@Binding var moveDistance: Double
@Binding var opacity: Double

var body: some View {
Button("奔跑吧,Peter") {
moveDistance += 100
opacity -= 0.3
}
.font(.title)
}
}

在傳入加了 $ 的 Binding 變數時,我們額外呼叫 animation(),之後當變數內容改變觸發畫面更新時將產生動畫。

struct ContentView: View {
@State private var moveDistance: Double = 0
@State private var opacity: Double = 1

var body: some View {
VStack {
RunButton(moveDistance: $moveDistance.animation(), opacity: $opacity.animation())
HStack {
Image("peter")
.offset(x: moveDistance)
.opacity(opacity)
Spacer()
}
}
}
}

如下圖所示,從 $moveDistance 呼叫 animation() 將回傳 Binding<Double>,雖然它的型別跟原來的 $moveDistance 一模一樣,但它已經有很大變化,它是有動畫功能的升級版。

同樣的,我們也可以客製動畫,我們可以在呼叫 animation() 時傳入指定的 Animation。

RunButton(moveDistance: $moveDistance.animation(.easeInOut(duration: 10)), opacity: $opacity.animation(.easeInOut(duration: 10)))

偵測 SwiftUI 動畫完成的 withAnimation(_:completionCriteria:_:completion:)

實現 SwiftUI 動畫時,modifier 的變化要在 view 已經加到畫面上後發生

不錯的動畫範例

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com