Explore SwiftUI animation
Swift & UI 进阶 20m

深入 SwiftUI 动画体系:Animatable、Animation 与 Transaction

Explore SwiftUI animation

2023年6月5日

在 Apple 官方观看视频

一句话判断

SwiftUI 动画不只是 withAnimation { }——这场 Session 从视图刷新机制讲起,把 Animatable 协议、Animation 曲线和 Transaction 上下文的协作关系梳理清楚了。

这场 Session 讲了什么

Session 以一个宠物投票 App 为例,系统讲解了 SwiftUI 动画的四个层次:视图刷新、Animatable 协议、Animation 曲线和 Transaction 上下文。

视图刷新的解剖。SwiftUI 追踪视图的依赖(@State 变量等)。当状态变化时,框架调用 body 生成新的视图值。新旧值之间的差异决定了需要更新哪些渲染属性。

Animatable 协议。这是 SwiftUI 判断”哪些属性需要动画插值”的机制。遵循 Animatable 的类型声明了 animatableData 属性——框架通过比较新旧 animatableData 来确定动画的起止值,然后在每一帧进行插值。SwiftUI 的内建类型(Color、CGFloat、CGPoint 等)已经遵循 Animatable。

Animation 曲线.easeInOut.spring().linear 等曲线定义了值随时间变化的方式。Spring 动画特别值得关注——它们基于物理模型,产生更自然的运动效果。response 参数控制动画速度,dampingFraction 控制弹性程度。

Transaction 上下文。每次视图更新都有一个关联的 Transaction,它携带了当前动画的配置信息(曲线、持续时间等)。withAnimation 的作用就是为代码块内的更新创建一个携带动画配置的 Transaction。animation 修饰符则为特定视图属性的变化创建 Transaction。

值得深挖的点

Animatable 是动画生效的前提:如果你自定义的视图参数类型没有遵循 Animatable,SwiftUI 不知道如何在旧值和新值之间插值,动画就不会生效。对于自定义类型,你需要手动实现 animatableData

Spring 动画的物理直觉response 值越小动画越快,dampingFraction 越接近 1 弹性越小(1.0 = 无弹性)。0.7-0.85 的 damping 通常产生最自然的”苹果风格”动画。

Transaction 的传播:Transaction 在视图层级中向下传播。子视图会继承父视图的动画配置,除非子视图自己覆盖了它。理解这个传播机制对调试复杂动画很有帮助。

代码片段

理解 SwiftUI 动画的四个层次:

struct PetAvatarView: View {
    let pet: Pet
    var isScaledUp: Bool  // 是否放大

    // Animatable: 告诉框架如何对缩放比例做插值
    var animatableData: CGFloat {
        get { isScaledUp ? 1.3 : 1.0 }
        set { /* 动画插值时由框架调用 */ }
    }

    var body: some View {
        Image(pet.imageName)
            .scaleEffect(isScaledUp ? 1.3 : 1.0)
            // animation 修饰符为这个视图的变化创建 Transaction
            .animation(.spring(response: 0.3, dampingFraction: 0.7),
                       value: isScaledUp)
    }
}

// withAnimation 创建携带动画配置的 Transaction
struct VoteButton: View {
    @State private var isScaledUp = false

    var body: some View {
        Button("投票") {
            // withAnimation 包裹状态变化,触发动画
            withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
                isScaledUp.toggle()
            }
        }
    }
}

最佳实践

  • 使用 withAnimation 包裹状态变化来触发动画,而非动画修饰符
  • Spring 动画比 ease-in-out 更自然,优先使用 .spring()
  • 自定义动画属性需要让类型遵循 Animatable
  • 动画曲线的参数要统一——同一个 App 内使用相同的 response 和 damping
  • 利用 Transaction 的传播机制统一管理动画配置

还有什么值得关注

  • Animation Phases(PhaseAnimator)的用法(下一场 Session 10157)
  • 自定义 Animation 曲线的实现方式
  • contentTransition 修饰符的过渡效果
  • 动画在 List、NavigationStack 等容器中的特殊行为
WWDC 2023