偵測 SwiftUI 動畫完成的 withAnimation(_:completionCriteria:_:completion:) — iOS 17 新功能

SwiftUI 提供方便的 function 幫我們快速實現各種動畫,像是 modifier animation(_:value:) & function withAnimation(_:_:)。

不過在 iOS 17 之前,有個我們很需要但 SwiftUI 實現會有點麻煩的功能,偵測動畫完成。

例如以下例子,舊版的 withAnimation 只能傳入想做的改變,沒有參數讓我們傳入動畫完成時觸發的 closure。

func withAnimation<Result>(
_ animation: Animation? = .default,
_ body: () throws -> Result
) rethrows -> Result
struct ContentView: View {
@State private var offset = CGSize.zero

var body: some View {
Image(.peter)
.offset(offset)
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) {
offset = CGSize(width: 100, height: 100)
}
}
}
}

假設我們想在動畫完成時讓 peter 回到初始位置,從前我們有兩種解法。

  • 解法 1: 使用 asyncAfter(deadline:qos:flags:execute:)。

動畫時間 1 秒鐘,表示動畫將在 1 秒後完成,因此我們設定 1 秒後將 offset 設為 .zero。 不過由於動畫不見得會精準在一秒後完成,所以動畫有時會有點誤差。

struct ContentView: View {
@State private var offset = CGSize.zero

var body: some View {
Image(.peter)
.offset(offset)
.onTapGesture {
withAnimation(.easeInOut(duration: 1)) {
offset = CGSize(width: 100, height: 100)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
offset = .zero
}
}
}
}
  • 自訂 AnimatableModifier。

自訂 AnimatableModifier 可以更精準地判斷動畫完成的時間,但是程式會複雜許多。

從 iOS 17 開始,我們有更簡單的方法偵測動畫完成,新的 function withAnimation(_:completionCriteria:_:completion:) 提供參數 completion 讓我們傳入動畫完成時觸發的 closure。

func withAnimation<Result>(
_ animation: Animation? = .default,
completionCriteria: AnimationCompletionCriteria = .logicallyComplete,
_ body: () throws -> Result,
completion: @escaping () -> Void
) rethrows -> Result

以下例子在參數 completion 設定 offset = .zero ,peter 將在動畫完成時瞬間回到初始位置。

import SwiftUI

struct ContentView: View {
@State private var offset = CGSize.zero

var body: some View {
Image(.peter)
.offset(offset)
.onTapGesture {
withAnimation {
offset = CGSize(width: 100, height: 100)
} completion: {
offset = .zero
}

}
}
}

有了 completion,我們將可實現多段動畫。當 peter 以動畫移動 (100, 100) 的距離後,再以動畫回到初始位置。

struct ContentView: View {
@State private var offset = CGSize.zero

var body: some View {
Image(.peter)
.offset(offset)
.onTapGesture {
withAnimation {
offset = CGSize(width: 100, height: 100)
} completion: {
withAnimation {
offset = .zero
}
}

}
}
}

要多少段動畫都不是問題,以下例子做了三段動畫,包含移動和縮放動畫。

  • 第一段: 移動 (100, 100) 的距離。
  • 第二段: 回到初始位置,大小變成 2 倍。
  • 第三段: 變回原本的大小。
import SwiftUI

struct ContentView: View {
@State private var offset = CGSize.zero
@State private var scale = 1.0

var body: some View {
Image(.peter)
.offset(offset)
.scaleEffect(scale)
.onTapGesture {
withAnimation(.easeInOut(duration: 3)) {
offset = CGSize(width: 100, height: 100)
} completion: {
withAnimation(.bouncy(duration: 3)) {
offset = .zero
scale = 2.0
} completion: {
withAnimation {
scale = 1.0
}
}
}

}
}
}

withAnimation(_:completionCriteria:_:completion:) 還有個型別是 AnimationCompletionCriteria 的參數 completionCriteria,它控制判斷動畫完成的機制,影響 completion 的觸發時機。

以下我們以 spring 動畫說明 completionCriteria 的影響。

struct ContentView: View {
@State private var offset = CGSize.zero
@State private var opacity = 1.0

var body: some View {
Image("peter")
.offset(offset)
.opacity(opacity)
.onTapGesture {
withAnimation(.spring(duration: 1, bounce: 0.9)) {
offset = CGSize(width: 100, height: 100)
} completion: {
withAnimation {
opacity = 0
}
}

}
}
}

completionCriteria 的預設值是 .logicallyComplete,意思是主要的動畫效果完成時就會觸發 completion ,然而動畫可能還在進行一些較不顯眼的部分。

如下圖所示,bounce 0.9 的 spring 動畫會來回震盪多次,logicallyComplete 將讓 completion 在震盪還沒結束前就觸發,所以震盪還在進行時 peter 就開始淡出。

若是 completionCriteria 傳入 .removed,completion 將在動畫完全結束時才觸發。

struct ContentView: View {
@State private var offset = CGSize.zero
@State private var opacity = 1.0

var body: some View {
Image(.peter)
.offset(offset)
.opacity(opacity)
.onTapGesture {
withAnimation(.spring(duration: 1, bounce: 0.9), completionCriteria: .removed) {
offset = CGSize(width: 100, height: 100)
} completion: {
withAnimation {
opacity = 0
}
}

}
}
}

因此 peter 將在震盪完全停止時才開始淡出。

透過參數 completion 我們可以實現多段動畫,然而當動畫愈多段時,程式會因層層的縮排大大增加複雜度。此時建議採用更適合設計多段動畫的 PhaseAnimator & KeyframeAnimator,相關說明可參考以下連結。

--

--

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

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