WWDC Quick Look 💓 By SwiftGGTeam
Animate symbols in your app

Animate symbols in your app

观看原视频

Highlight

iOS 17 和 macOS Sonoma 引入了 Symbols framework,为所有 SF Symbols(包括自定义符号)提供统一的动画 API。开发者通过 .symbolEffect() 修饰符和点分隔的效果名称,就能在 SwiftUI、UIKit、AppKit 中添加 Bounce、Pulse、Variable Color 等动画效果。

核心内容

符号动画的四种行为

以前想让图标动起来,需要手写 UIView.animatewithAnimation,还要自己计算关键帧。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
            )
    }
}

关键点:

  • isActivetrue 时动画开始播放
  • 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 支持 addSymbolEffect
  • UIControlUIBarButtonItem 新增 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

评论

GitHub Issues · utterances