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>