WWDC Quick Look 💓 By SwiftGGTeam
Animate with springs

Animate with springs

观看原视频

Highlight

SwiftUI 在 iOS 17 将默认动画从 easeInOut 改为基于物理的 spring,并引入 duration + bounce 两个直观参数替代传统的 mass/stiffness/damping,同时提供了 Spring 模型类型让开发者可以在自定义动画中复用苹果的弹簧计算逻辑。

核心内容

为什么弹簧动画更好

Jacob 从 SwiftUI 团队出发,用开关按钮的旋钮动画作为例子。动画的核心价值之一是提供连续性:物体从 A 到 B 时,位置和速度都应该连续变化,否则会感觉突兀。

Ease In/Out 动画在静态起始时表现不错,但配合手势使用时有一个致命缺陷:无法表示初始速度。当用户拖拽后松手,easeInOut 动画会突兀地停止再重新开始。Linear 动画在起始和结束点都有速度跳跃,同样不适合。

弹簧动画有两个核心优势:

  1. 速度和位置都连续:弹簧可以接受任意初始速度,动画会从手势结束时的速度自然过渡
  2. 运动更自然:基于真实物理世界的弹簧模型,缓慢减速到停止,符合人类对物体运动的直觉

弹簧的物理模型

08:35)弹簧动画由三个物理属性定义:

  • mass(质量):物体的质量
  • stiffness(刚度):弹簧的硬度
  • damping(阻尼):系统的摩擦力

但这些参数对开发者不够直观。SwiftUI 引入了更友好的参数:

  • duration(持续时间):动画的感知持续时间
  • bounce(弹跳):弹跳程度,从 -1.0 到 1.0

三种弹簧类型:

  • bouncy(bounce > 0):过阻尼,会 overshoot 目标位置
  • smooth(bounce = 0):临界阻尼,缓慢渐近到目标
  • flattened(bounce < 0):欠阻尼,更平坦地接近目标

多属性协作

08:05)iOS 上的 App 启动动画是弹簧动画的经典案例。不同属性使用不同的弹簧参数、不同的起始和结束时间,组合出自然流畅的效果。不同属性不需要同时开始和结束,这种”不齐整”反而让动画感觉更真实。

详细内容

使用 Spring 预设

18:00)SwiftUI 提供了三种内置弹簧预设:

withAnimation(.snappy) {
    // Changes
}

关键点:

  • .snappy:少量弹跳,适合大多数交互
  • .smooth:无弹跳,适合微妙的变化
  • .bouncy:更多弹跳,适合活泼的交互

自定义预设参数:

withAnimation(.snappy(duration: 0.4)) {
    // 自定义持续时间
}

withAnimation(.snappy(extraBounce: 0.1)) {
    // 在预设基础上增加弹跳
}

自定义 Spring

18:37)完全自定义弹簧:

withAnimation(.spring(duration: 0.6, bounce: 0.2)) {
    // Changes
}

// UIKit
UIView.animate(duration: 0.6, bounce: 0.2) {
    // Changes
}

// Core Animation
let animation = CASpringAnimation(perceptualDuration: 0.6, bounce: 0.2)

关键点:

  • durationbounce 参数在 SwiftUI、UIKit 和 Core Animation 中统一
  • bounce 范围是 -1.0 到 1.0
  • 0 表示 smooth,正值表示 bouncy,负值表示 flattened

Spring 模型类型

18:57Spring 类型可以独立使用:

let mySpring = Spring(duration: 0.5, bounce: 0.2)
let (mass, stiffness, damping) = (mySpring.mass, mySpring.stiffness, mySpring.damping)

用传统参数创建:

let otherSpring = Spring(mass: 1, stiffness: 100, damping: 10)
withAnimation(.spring(otherSpring)) {
    // Changes
}

关键点:

  • 可以在 duration/bounce 和 mass/stiffness/damping 之间转换
  • 参数转换公式:
    • stiffness = (2π ÷ duration)²
    • damping = 1 - 4π × bounce ÷ duration(bounce ≥ 0)
    • damping = 4π ÷ (duration + 4π × bounce)(bounce < 0)

评估 Spring

19:35)可以查询弹簧在任意时间点的状态:

let mySpring = Spring(duration: 0.4, bounce: 0.2)
let value = mySpring.value(target: 1, time: time)
let velocity = mySpring.velocity(target: 1, time: time)

关键点:

  • value 返回弹簧在指定时间的位置
  • velocity 返回弹簧在指定时间的速度
  • 适合用于自定义动画、模拟或数据可视化

在自定义动画中使用 Spring

20:15)在 CustomAnimation 中复用弹簧逻辑:

func animate<V: VectorArithmetic>(
    value: V, time: Double, context: inout AnimationContext<V>
) -> V? {
    spring.value(
        target: value, initialVelocity: context.initialVelocity,
        time: effectiveTime(time: time, context: context))
}

关键点:

  • 调用 spring.value 获取插值结果
  • 传入 initialVelocity 保留手势速度
  • effectiveTime 处理时间映射

选择 Bounce 值

21:07)不同 bounce 值的感觉:

// 无弹跳 - 最通用
withAnimation(.spring(duration: 0.5)) {
    isActive.toggle()
}

// 小弹跳 - 更有活力
withAnimation(.spring(duration: 0.5, bounce: 0.15)) {
    isActive.toggle()
}

// 大弹跳 - 更活泼
withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
    isActive.toggle()
}

关键点:

  • 不确定时用 bounce 0(smooth),这是最通用的选择
  • 想要活泼感时增加 bounce
  • 手势结束的动画适合用 bounce,因为更物理
  • 超过 0.4 的 bounce 可能过于夸张,谨慎使用
  • 保持 App 内动画风格一致

Settling Duration

16:45)弹簧动画理论上永远不会完全停止,只是运动越来越小。SwiftUI 使用 “settling duration” 来决定何时移除动画——当运动小到不可察觉时。

但要注意:settling duration 是不可预测的。如果需要在动画”基本结束”时做 UI 变更,使用 SwiftUI 新的 completion handler,它基于 perceptual duration 而非 settling duration。

核心启发

  1. 全局替换 easeInOut

    • 做什么:将 App 中所有 withAnimation.animation() 的默认调用改为显式 spring
    • 为什么值得做:iOS 17 默认已经是 smooth spring,但显式指定让意图更清晰,且可以微调参数
    • 怎么开始:搜索项目中所有 .easeInOut 和裸 withAnimation,替换为 .spring(duration:bounce:)
  2. 手势驱动的弹性交互

    • 做什么:为拖拽、滑动等手势添加弹簧收尾动画
    • 为什么值得做:弹簧自动继承手势速度,松手后的动画自然流畅,不需要手动计算速度
    • 怎么开始:在 onEnded 中用 withAnimation(.spring) 设置最终状态,SwiftUI 自动处理速度传递
  3. 可中断的动画系统

    • 做什么:让动画可以被新动画打断并平滑过渡
    • 为什么值得做:弹簧动画在被打断时会保留当前速度作为新动画的初始速度,用户体验连贯
    • 怎么开始:确保使用 spring 动画,当新状态到来时直接调用 withAnimation(.spring) 更新状态
  4. 品牌动效设计系统

    • 做什么:为团队定义一套统一的动画参数规范
    • 为什么值得做:duration + bounce 两个参数足够表达品牌调性(严肃 vs 活泼),且全团队容易理解
    • 怎么开始:定义 extension Animation { static let brand = .spring(duration: 0.4, bounce: 0.1) },全 App 统一使用
  5. Spring 驱动的数据可视化

    • 做什么:用 Spring 模型计算值来驱动自定义图表或游戏物理
    • 为什么值得做:可以复用苹果优化的弹簧数学,不需要自己实现物理模拟
    • 怎么开始:创建 Spring 实例,在 TimelineView 或游戏循环中调用 spring.value(target:time:) 获取当前值

关联 Session

评论

GitHub Issues · utterances