Wind your way through advanced animations in SwiftUI
Swift & UI 进阶 20m

SwiftUI 高级动画:Animation Phases 与 KeyframeAnimator

Wind your way through advanced animations in SwiftUI

2023年6月5日

在 Apple 官方观看视频

一句话判断

SwiftUI 新增了 PhaseAnimator 和 KeyframeAnimator 两套动画 API——前者适合”循环播放”和”事件触发”的多步动画,后者适合精确控制每个关键帧的时间和曲线。如果你的动画需求超过了 withAnimation 的能力范围,看这场就对了。

这场 Session 讲了什么

Session 介绍了 SwiftUI 中两个新的动画构建工具,专门解决”不只是从 A 状态过渡到 B 状态”的复杂动画需求。

Animation Phases(PhaseAnimator)。传统的 withAnimation 只处理”旧状态到新状态”的过渡。PhaseAnimator 允许你定义多个阶段(phases),SwiftUI 自动在阶段之间循环切换。每个阶段有自己的持续时间和动画曲线。两种典型场景:循环动画(如加载指示器持续旋转)和事件驱动动画(如按钮被点击后播放一个脉冲效果)。

KeyframeAnimator。关键帧动画允许你为每个动画属性定义独立的轨迹(track),每个轨迹有自己的一组关键帧,每个关键帧有独立的时间和动画曲线。不同轨迹的关键帧可以交错——比如缩放和旋转在时间上不完全对齐,产生更丰富的视觉效果。

与传统动画的关系。PhaseAnimator 和 KeyframeAnimator 不替代 withAnimation,而是补充它。简单的状态过渡继续用 withAnimation,需要多步骤或精细控制的动画用新 API。

值得深挖的点

PhaseAnimator 的触发模式。循环模式下,动画完成后自动从最后一个阶段跳回第一个阶段继续播放。事件触发模式下,动画完成后停在最后一个阶段,等待下一次触发。两种模式的选择取决于你的使用场景——持续性效果用循环,一次性反馈用触发。

关键帧的时间模型:关键帧定义的不是”从何时开始”,而是”何时到达目标值”。两个关键帧之间的动画由曲线控制。这意味着你可以在一条轨迹上定义密集的关键帧来创造加速-减速效果,而另一条轨迹只定义少量关键帧保持匀速。

多轨迹的独立性:每条轨迹有自己的关键帧集和时间线。你可以让缩放在 0.3 秒内完成,旋转在 0.5 秒内完成,两条轨迹并行运行互不干扰。这比嵌套多个 withAnimation 调用要优雅得多。

代码片段

使用 PhaseAnimator 创建循环动画:

// 循环脉冲动画
PhaseAnimator([false, true]) { phase in
    Circle()
        .fill(phase ? Color.red : Color.blue)
        .scaleEffect(phase ? 1.2 : 0.8)
} animation: { phase in
    // 每个阶段的动画配置
    switch phase {
    case false: .easeInOut(duration: 0.6)
    case true:  .spring(response: 0.3, dampingFraction: 0.5)
    }
}

使用 KeyframeAnimator 创建多轨迹动画:

KeyframeAnimator(
    initialValue: AnimationValues(),
    repeating: true
) { values in
    CatView()
        .scaleEffect(values.scale)
        .rotationEffect(values.rotation)
        .offset(y: values.verticalOffset)
} keyframes: { _ in
    // 缩放轨迹
    KeyframeTrack(\.scale) {
        CubicKeyframe(1.0, duration: 0.3)   // 正常大小
        CubicKeyframe(1.3, duration: 0.2)   // 放大
        CubicKeyframe(1.0, duration: 0.3)   // 恢复
    }

    // 旋转轨迹(时间线独立于缩放)
    KeyframeTrack(\.rotation) {
        CubicKeyframe(.zero, duration: 0.4)
        CubicKeyframe(.degrees(15), duration: 0.15)
        CubicKeyframe(.degrees(-15), duration: 0.15)
        CubicKeyframe(.zero, duration: 0.2)
    }

    // 垂直偏移轨迹
    KeyframeTrack(\.verticalOffset) {
        CubicKeyframe(0, duration: 0.2)
        CubicKeyframe(-20, duration: 0.3)   // 跳起
        CubicKeyframe(0, duration: 0.3)     // 落下
    }
}

最佳实践

  • 简单的状态过渡继续用 withAnimation,不要为了用新 API 而过度工程化
  • 循环动画用 PhaseAnimator 的循环模式,一次性效果用触发模式
  • 关键帧动画中,为每个视觉属性(缩放、旋转、偏移)定义独立轨迹
  • 不同轨迹使用不同的持续时间和曲线,创造错落有致的效果
  • 在真机上测试动画性能——复杂的关键帧动画可能影响帧率

还有什么值得关注

  • PhaseAnimator 与 onAppear 配合使用的触发时机
  • CubicKeyframe vs SpringKeyframe 的选择
  • 关键帧动画在 List 等容器中的表现
  • 动画可中断性——PhaseAnimator 中途触发新的动画会如何处理
WWDC 2023