利用 SwiftUI 的 symbolEffect 實現 SF Symbol 動畫 — iOS 17 新功能
從 iOS 17 開始,只要一行程式即可為 SF Symbol 添加活潑的動畫效果。在研究程式前,我們可以先用 SF Symbols App 認識 SF Symbol 各種生動的動畫效果。
認識 SF Symbol 的動畫效果後,接下來讓我們學習如何在 SwiftUI 實現 SF Symbol 動畫,以下我們以 SF Symbol wifi 為例說明。
import SwiftUI
struct ContentView: View {
var body: some View {
Image(systemName: "wifi")
}
}
- 永不停止的動畫。
- 控制動畫的開始和停止。
- 設定動畫的細節。
- 設定動畫的速度。
- 一次性的動畫。
- 設定動畫的次數。
- 換圖的 replace 動畫。
- 出現和消失的動畫。
永不停止的動畫
透過 modifier symbolEffect(_:options:isActive:)
實現 SF Symbol 的動畫效果。
如下圖所示,我們有多種效果可選擇。
- Appear(出現)。
- Bounce(彈跳)。
- Scale(縮放)。
- Wiggle(擺動,iOS 18 以上)。
- Rotate(旋轉,iOS 18 以上)。
- Breathe(呼吸,包含縮放和淡入淡出,iOS 18 以上)。
- Pulse(脈衝,淡入淡出)。
- Variable Color(照順序調整不同 layer 的透明度。)
- Replace(取代,換新的圖案)。
- Disappear(消失)。
symbolEffect(_:options:isActive:)
通常會產生永不停止的動畫,因為 effect 的參數型別是 IndefiniteSymbolEffect,等下我們會再介紹只做一次的動畫寫法。
- wifi 不斷擺動。
struct ContentView: View {
var body: some View {
Image(systemName: "wifi")
.symbolEffect(.wiggle)
}
}
- wifi &square.and.arrow.up.circle.fill 同時不斷擺動。
將 symbolEffect 套用在 HStack 時,它也會套用在 HStack 包含的 Image 上。
struct ContentView: View {
var body: some View {
HStack {
Image(systemName: "wifi")
Image(systemName: "square.and.arrow.up.circle.fill")
}
.symbolEffect(.wiggle)
}
}
控制動畫的開始和停止
剛剛的寫法會產生永不停止的動畫,若想手動控製動畫的開始跟停止,可另外透過參數 isActive。
struct ContentView: View {
@State private var isAnimating = false
var body: some View {
VStack(spacing: 10) {
Image(systemName: "wifi")
.symbolEffect(.pulse, isActive: isAnimating)
Button("開始動畫") {
isAnimating = true
}
Button("停止動畫") {
isAnimating = false
}
}
}
}
設定動畫的細節
每種動畫可設定不同的細節,比方 variableColor 可以 iterative 或 cumulative。
- iterative(反覆)。
依序加深 layer 的透明度,同時間只會有一個 layer 的顏色是深色,其它 layer 是淺色。
struct ContentView: View {
var body: some View {
Image(systemName: "wifi")
.symbolEffect(.variableColor.iterative)
}
}
如下圖所示,一次只有一條 wifi 的線條是深色。
- cumulative(累積)。
依序加深 layer 的透明度,效果會累積,因此 wifi 深色的線條將從一條變成兩條,然後再變成三條。
struct ContentView: View {
var body: some View {
Image(systemName: "wifi")
.symbolEffect(.variableColor.cumulative)
}
}
設定動畫的速度
參數 options 傳入 .speed(0.1)
可控制動畫的速度,0.1 表示速度變慢,變成原來的 0.1 倍。
struct ContentView: View {
var body: some View {
Image(systemName: "wifi")
.symbolEffect(.pulse, options: .speed(0.1))
}
}
一次性的動畫
透過 modifier symbolEffect(_:options:value:)
實現一次性的動畫,參數 effect 的型別是 DiscreteSymbolEffect,它將透過 value 的改變觸發動畫。
以下程式在按下按鈕時改變 value,觸發 wifi 做一次彈跳動畫。
struct ContentView: View {
@State private var value = 0
var body: some View {
VStack(spacing: 10) {
Image(systemName: "wifi")
.symbolEffect(.bounce, value: value)
Button("開始動畫") {
value += 1
}
}
}
}
設定動畫的次數
參數 options 傳入 .repeat(.periodic(2, delay: 0) )
可設定動畫的次數,傳入 2 表示做 2 次動畫。
- 寫法 1。
按下按鈕時做兩次動畫。
struct ContentView: View {
@State private var value = 0
var body: some View {
VStack {
Image(systemName: "wifi")
.symbolEffect(.breathe, options: .repeat(.periodic(2, delay: 0)), value: value)
Button("開始動畫") {
value += 1
}
}
}
}
- 寫法 2。
畫面出現時做兩次動畫。
struct ContentView: View {
var body: some View {
Image(systemName: "wifi")
.symbolEffect(.breathe, options: .repeat(.periodic(2, delay: 0)))
}
}
換圖的 replace 動畫
呼叫 modifier contentTransition(_:)
,傳入 .symbolEffect(.replace)
。
struct ContentView: View {
@State private var isMuted = false
var body: some View {
Button {
isMuted.toggle()
} label: {
Image(systemName: isMuted ? "bell.slash" : "bell")
.contentTransition(.symbolEffect(.replace))
}
}
}
replace 還可以調整細節,設定不同的取代效果。
出現和消失的動畫
以下示範兩種做法。
- 方法 1: 用 isActive 觸發動畫,元件消失時依然佔著空間,因此畫面排版不受影響。
struct ContentView: View {
@State private var isActive = false
var body: some View {
VStack(spacing: 10) {
Image(systemName: "wifi")
.symbolEffect(.disappear, isActive: isActive)
Button("隱藏") {
isActive = true
}
}
}
}
- 方法 2: 利用 transition 搭配 .symbolEffect(.disappear) 實現消失動畫,元件消失時不佔空間,因此畫面排版會受影響。
struct ContentView: View {
@State private var isWifiHidden = false
var body: some View {
VStack(spacing: 10) {
if !isWifiHidden {
Image(systemName: "wifi")
.transition(.symbolEffect(.disappear))
}
Button("隱藏") {
isWifiHidden = true
}
}
}
}