SwiftUI 提供了 symbolEffect 修饰符来为 SF 符号添加内置动画效果,并几乎毫不费力地产生真正的愉悦感。调用方式很简单,主要是通过 symbolEffect 修饰符实现的。
symbolEffect 修饰符,在后面类型参数上填上想要的类型Image(systemName: "ellipsis")
	.symbolEffect(.pulse)
	
// 有些动画效果是有下级选项的,这种情况只需要用链式选择即可
Image(systemName: "ellipsis")
	.symbolEffect(.scale.up)
我们可以通过设置参数 options ,来控制 SF Symbol 动画的细节。
// 例如控制动画的速度(值越大速度越快)
Image(systemName: "wand.and.rays")
	.symbolEffect(.variableColor.iterative.reversing, options:.speed(3))
	
// 如果要更多设置,同样使用链式语法即可,例如我们再控制该动画的重复次数
Image(systemName: "bell.and.waves")
	.symbolEffect(.variableColor.iterative.reversing, options:.speed(3).repeat(3))
struct ContentView: View {
    @State private var petCount = 0
    var body: some View {
        Button {
            petCount += 1
        } label: {
            Label("Pet the Dog", systemImage: "dog")
        }
        .symbolEffect(.bounce, value: petCount)
    }
}
// 或者这样
@State var isTapped = false
Image(systemName: "bell.and.waves")
	.symbolEffect(.variableColor, options:.speed(3), value:isTapped)
<aside> 💡
.symbolEffect 修饰符可以同时添加多个,不冲突。比如有一个用于让动画A一直执行,另一个控制点击的时候才执行动画B。
</aside>
.appeartrue,可以不用填,所以很多动画上来就执行一遍。如果不希望动画自动执行,可以将其设置为 false。Image(systemName: "face.smiling")
	.symbolEffect(.appear, isActive:isActive)
在 SF Symbols 7 和 iOS 26 中,Apple 推出了 “绘制动画”功能 ,这项新功能让图标栩栩如生。与传统的淡入淡出、缩放或弹跳动画不同,绘画动画模拟了用笔绘制符号的自然流畅性,从而创建了更具吸引力和表现力的用户界面。
SwiftUI 中的绘制动画是使用 symbolEffect(_: options: isActive:) 修饰符实现的。
效果类型主要是 drawOn 和 drawOff 两种,前者是将符号绘制出来;后者是将符号抹掉。
// Draw On: animates the symbol appearing
Image(systemName: "checkmark.circle")
    .symbolEffect(.drawOn, isActive: isComplete)
// Draw Off: animates the symbol disappearing
Image(systemName: "checkmark.circle")
    .symbolEffect(.drawOff, isActive: isHidden)
SF 符号由多个图层构成,这些图层定义了符号的不同部分,绘制动画会以不同的方式绘制这些图层。在实现绘制动画时主要有三种不同的方法
.wholeSymbol  整个符号:该方式下,SF 符号的所有图层会同时动画,创建统一的绘图效果,整个符号似乎被同时绘制。.byLayer  逐层:该方式下,图层以交错时间进行动画(默认行为)。每一层都会在前一层之后稍稍开始绘制,从而营造出深度。.individually 单独:图层按顺序动画,等待每个图层完成后再开始下一个图层。这样可以创造出最刻意、最循序渐进的绘制效果。// Default staggered animation
Image(systemName: "square.and.arrow.up")
    .symbolEffect(.drawOn.byLayer, isActive: showSquare)
// All layers at once
Image(systemName: "square.and.arrow.up")
    .symbolEffect(.drawOn.wholeSymbol, isActive: showSquare)
// Sequential layer animation
Image(systemName: "square.and.arrow.up")
    .symbolEffect(.drawOn.individually, isActive: showSquare)
还可以使用其他选项自定义动画行为,例如:
// Non-repeating animation (runs once)
.symbolEffect(.drawOn, options: .nonRepeating, isActive: showSquare)
// Faster animation speed
.symbolEffect(.drawOn, options: .speed(2.0), isActive: showSquare)
// Repeating animation
.symbolEffect(.drawOn, options: .repeat(.continuous), isActive: showSquare)
isActive 参数控制动画何时应处于活动状态并需要状态管理。在下面的示例中,当 isDrawing 的值为 true 时,动画将运行:
struct ContentView: View {
    // Controls animation state
    @State private var isDrawing = false
    var body: some View {
        Image(systemName: "signature")
            .symbolEffect(.drawOn, isActive: isDrawing)
        Button("Draw") {
            // Activates and deactivates the animation
            isDrawing.toggle()
        }
    }
}
通过将 symbolEffect(_:options:isActive:) 与适当的状态管理相结合,就解锁了 SF Symbols 7 绘制动画的强大功能。无论选择哪种方式 ,用户都将体验到手绘的质感,让界面栩栩如生!
使用该修饰符可以删除动画效果。
struct WiFiButton: View {
    @State var isSearchingForWiFi = false
    @State var runSearchForWifiAnimation = false
    var body: some View {
        Button {
            isSearchingForWiFi.toggle()
        } label: {
            Label(title: {
                Text("Search for WiFi")
            }) {
                Image(systemName: "wifi")
                    .symbolEffectsRemoved(!isSearchingForWiFi)
                    .symbolEffect(.variableColor, options: .repeat(.periodic), value: runSearchForWifiAnimation)
                    .onChange(of: isSearchingForWiFi) { _, newValue in
                        if newValue {
                            runSearchForWifiAnimation.toggle()
                        }
                    }
            }
        }
    }
}
// 注意,这里将 isSearchingForWiFi 与元件效果分离,因为我们需要单独处理动画状态。否则,SwiftUI 不会重绘视图来重置动画,并且会永远循环。
如果您希望保持视图不变,仅更改其内容(例如根据用户交互切换固定标签的图标),例如播放按钮,点击后从三角形变成暂停符号,那么应该使用 .contentTransition  修改器加上 .replace 动画来使一个图标淡出而另一个图标到达。
Image(systemName: "play.fill")
	.contentTransition(
			.symbolEffect(.replace)
	)
// 这时我们可能会发现动画并没有执行,因为它需要通过值的变化来驱动	
@State var isTapped = false
Image(systemName: isTapped ? "pause.fill" : "play.fill")
	.contentTransition(
			.symbolEffect(.replace)
	)
	.onTapGesture{
			isTapped.toggle()
	}
<aside> 💡
当使用替换图标时,有时会发现图标有跳动,这是因为前后的图标框架不一致导致的。这里可以添加 .frame() 修饰符来避免这个问题。
</aside>