.symbolEffect 修饰符

SwiftUI 提供了 symbolEffect 修饰符来为 SF 符号添加内置动画效果,并几乎毫不费力地产生真正的愉悦感。调用方式很简单,主要是通过 symbolEffect 修饰符实现的。

1. 参数 动画类型

Image(systemName: "ellipsis")
	.symbolEffect(.pulse)
	
// 有些动画效果是有下级选项的,这种情况只需要用链式选择即可
Image(systemName: "ellipsis")
	.symbolEffect(.scale.up)

2. 参数 options

我们可以通过设置参数 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))

3. 参数 value

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>

4. 参数 isActive

Image(systemName: "face.smiling")
	.symbolEffect(.appear, isActive:isActive)

SF Symbols 绘制动画(iOS26)

在 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 符号由多个图层构成,这些图层定义了符号的不同部分,绘制动画会以不同的方式绘制这些图层。在实现绘制动画时主要有三种不同的方法

// 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

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 绘制动画的强大功能。无论选择哪种方式 ,用户都将体验到手绘的质感,让界面栩栩如生!


symbolEffectsRemoved

使用该修饰符可以删除动画效果。

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

如果您希望保持视图不变,仅更改其内容(例如根据用户交互切换固定标签的图标),例如播放按钮,点击后从三角形变成暂停符号,那么应该使用 .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>