Highlight
iOS 17 和 macOS Sonoma 引入了 Symbols framework,为所有 SF Symbols(包括自定义符号)提供统一的动画 API。开发者通过
.symbolEffect()修饰符和点分隔的效果名称,就能在 SwiftUI、UIKit、AppKit 中添加 Bounce、Pulse、Variable Color 等动画效果。
核心内容
符号动画的四种行为
以前想让图标动起来,需要手写 UIView.animate 或 withAnimation,还要自己计算关键帧。Symbols framework 把这件事简化了:你只需要声明想要什么效果,系统负责动画细节。
所有效果可以归为四种行为(04:25):
| 行为 | 说明 | 代表效果 |
|---|---|---|
| Discrete(离散) | 一次性动画,播放完自动结束 | Bounce |
| Indefinite(持续) | 改变符号状态并保持,需显式移除 | Scale、Variable Color |
| Transition(过渡) | 控制符号的显示和隐藏 | Appear、Disappear |
| Content Transition(内容过渡) | 在两个符号之间切换 | Replace |
每种行为对应一个 Swift protocol。效果通过遵循这些 protocol 来声明自己支持哪些行为。比如 Variable Color 同时支持 Indefinite 和 Discrete,具体行为取决于你怎么用 API。
点分隔的效果名称
Symbols framework 的一个设计亮点是效果名称用点分隔语法(02:03):
.bounce
.variableColor.iterative.reversing
.scale.up
这些是真正的 Swift 代码,不是字符串。Xcode 会自动补全每个部分,配置错误在编译期就能报错。你可以在 SF Symbols app 的 Animation 标签页里预览效果,然后直接复制效果名称到代码里。
在 SwiftUI 中使用
SwiftUI 新增了 symbolEffect 修饰符(05:11)。最简单的用法是直接在 Image 上添加效果:
Image(systemName: "wifi.router")
.symbolEffect(.variableColor.iterative.reversing)
.symbolEffect(.scale.up)
效果可以叠加。上面的代码同时应用了 Variable Color 动画和放大效果。
在 UIKit/AppKit 中使用
UIKit 和 AppKit 的 UIImageView/NSImageView 新增了 addSymbolEffect 方法(05:30):
let imageView: UIImageView = ...
imageView.addSymbolEffect(.variableColor.iterative.reversing)
imageView.addSymbolEffect(.scale.up)
详细内容
Indefinite 效果:控制动画的启停
Indefinite 效果会持续播放,直到被移除。SwiftUI 通过 isActive 参数控制(06:49):
struct ContentView: View {
@State var isConnectingToInternet: Bool = true
var body: some View {
Image(systemName: "wifi.router")
.symbolEffect(
.variableColor.iterative.reversing,
isActive: isConnectingToInternet
)
}
}
关键点:
isActive为true时动画开始播放isActive变为false时动画平滑结束- 适合表示”进行中”的状态,比如网络连接、文件上传
在 UIKit/AppKit 中,用 removeSymbolEffect(ofType:) 停止动画(07:09):
imageView.addSymbolEffect(.variableColor.iterative.reversing)
// 连接成功后停止动画
imageView.removeSymbolEffect(ofType: .variableColor)
Discrete 效果:响应事件的一次性动画
Discrete 效果在值变化时触发一次。Bounce 是典型的 Discrete 效果(08:26):
struct ContentView: View {
@State var bounceValue: Int = 0
var body: some View {
VStack {
Image(systemName: "antenna.radiowaves.left.and.right")
.symbolEffect(
.bounce,
options: .repeat(2),
value: bounceValue
)
Button("Animate") {
bounceValue += 1
}
}
}
}
关键点:
value参数变化时触发动画options: .repeat(2)让动画重复播放 2 次- 适合响应用户操作,比如点击按钮后的反馈
Pulse 和 Variable Color 也支持 Discrete 行为。当用 value 触发时,它们会播放一次性的动画序列。
Replace 效果:符号切换动画
Replace 效果让两个符号之间的切换带有动画过渡(09:40):
struct ContentView: View {
@State var isPaused: Bool = false
var body: some View {
Button {
isPaused.toggle()
} label: {
Image(systemName: isPaused ? "pause.fill" : "play.fill")
.contentTransition(.symbolEffect(.replace.offUp))
}
}
}
关键点:
contentTransition(.symbolEffect(.replace.offUp))让符号切换时带有向上飞出的替换动画.offUp、.offDown等选项控制替换方向- 适合播放/暂停、收藏/取消收藏等状态切换场景
UIKit 中用 setSymbolImage(_:contentTransition:) 实现同样效果(09:57):
let pauseImage = UIImage(systemName: "pause.fill")!
imageView.setSymbolImage(pauseImage, contentTransition: .replace.offUp)
Appear 和 Disappear:两种使用方式
Appear 和 Disappear 支持两种行为模式(10:15):
方式一:Indefinite 行为(不改变布局)
符号在原地消失或出现,周围视图的布局不变:
Image(systemName: "moon.stars")
.symbolEffect(.disappear, isActive: isMoonHidden)
方式二:Transition 行为(改变布局)
符号从视图层级中插入或移除,周围视图会重新布局:
if !isMoonHidden {
Image(systemName: "moon.stars")
.transition(.symbolEffect(.disappear.down))
}
关键点:
- Indefinite 方式适合徽章、状态标记的显隐
- Transition 方式适合条件渲染的视图
.automatic过渡效果会自动选择最合适的动画
UIKit 中可以用 completion handler 在动画结束后移除视图(12:59):
imageView.addSymbolEffect(.disappear) { context in
if let imageView = context.sender as? UIImageView, context.isFinished {
imageView.removeFromSuperview()
}
}
效果传播和禁用
SwiftUI 中 symbolEffect 修饰符会向下传播(14:19):
VStack {
Image(systemName: "figure.walk")
.symbolEffectsRemoved()
Image(systemName: "car")
Image(systemName: "tram")
}
.symbolEffect(.pulse)
关键点:
- 父视图上的
.symbolEffect(.pulse)会让所有子视图中的符号都脉冲 .symbolEffectsRemoved()可以阻止特定视图继承效果- 适合在列表或工具栏中统一控制动画
无动画应用效果
有时你需要让符号初始就处于某种效果状态,但不要动画过渡。SwiftUI 中用 Transaction 禁用动画(14:55):
Image(systemName: "iphone.radiowaves.left.and.right")
.symbolEffect(.scale.up, isActive: isScaledUp)
.onAppear {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
isScaledUp = true
}
}
UIKit/AppKit 中直接用 animated: false 参数:
imageView.addSymbolEffect(.disappear, animated: false)
Variable Value 的平滑过渡
iOS 16 引入的 Variable Value 在 iOS 17 中支持自动动画(15:44):
struct ContentView: View {
@State var signalLevel: Double = 0.5
var body: some View {
Image(systemName: "wifi", variableValue: signalLevel)
}
}
SwiftUI 中 variableValue 变化时会自动产生交叉淡入淡出动画。UIKit 中用 setSymbolImage(_:contentTransition: .automatic) 实现同样效果。
UIKit 控件的内置动画
iOS 17 中部分 UIKit 控件自带符号动画(13:47):
UISlider的滑块到达两端时会弹跳UIBarButtonItem支持addSymbolEffectUIControl和UIBarButtonItem新增isSymbolAnimationEnabled属性控制是否播放动画
核心启发
给网络状态指示器加上 Variable Color 动画
- 做什么:在导航栏或工具栏放一个 Wi-Fi 或路由器图标,连接时播放 Variable Color 动画
- 为什么值得做:这是 Discrete + Indefinite 效果的经典场景,连接中持续播放,连接成功后平滑停止
- 怎么开始:用
.symbolEffect(.variableColor.iterative.reversing, isActive: isConnecting)
用 Replace 效果优化播放控制按钮
- 做什么:把播放/暂停按钮的图标切换加上 Replace 动画
- 为什么值得做:Replace 效果让两个符号之间的切换有方向感,比直接替换图片更有质感
- 怎么开始:用
.contentTransition(.symbolEffect(.replace.offUp))
给列表操作加上 Bounce 反馈
- 做什么:用户点击收藏、删除等按钮时,图标弹跳一次
- 为什么值得做:Bounce 是 Discrete 效果,天然适合作为操作反馈,比静态高亮更生动
- 怎么开始:用
.symbolEffect(.bounce, value: tapCount),每次点击增加tapCount
用 Disappear 过渡做条件视图
- 做什么:条件显示的图标用
.transition(.symbolEffect(.disappear))添加出现/消失动画 - 为什么值得做:Transition 行为会改变布局,适合徽章、提示标记的显隐
- 怎么开始:在
if条件里的 Image 上添加.transition(.symbolEffect(.automatic))
关联 Session
- Create animated symbols — 在 SF Symbols app 中创建和预览动画符号
- What’s new in SF Symbols 5 — SF Symbols 5 的新特性总览
- Animate with springs — SwiftUI 弹簧动画的底层原理
评论
GitHub Issues · utterances