Highlight
SwiftUI 在 iOS 17 将默认动画从 easeInOut 改为基于物理的 spring,并引入 duration + bounce 两个直观参数替代传统的 mass/stiffness/damping,同时提供了
Spring模型类型让开发者可以在自定义动画中复用苹果的弹簧计算逻辑。
核心内容
为什么弹簧动画更好
Jacob 从 SwiftUI 团队出发,用开关按钮的旋钮动画作为例子。动画的核心价值之一是提供连续性:物体从 A 到 B 时,位置和速度都应该连续变化,否则会感觉突兀。
Ease In/Out 动画在静态起始时表现不错,但配合手势使用时有一个致命缺陷:无法表示初始速度。当用户拖拽后松手,easeInOut 动画会突兀地停止再重新开始。Linear 动画在起始和结束点都有速度跳跃,同样不适合。
弹簧动画有两个核心优势:
- 速度和位置都连续:弹簧可以接受任意初始速度,动画会从手势结束时的速度自然过渡
- 运动更自然:基于真实物理世界的弹簧模型,缓慢减速到停止,符合人类对物体运动的直觉
弹簧的物理模型
(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)
关键点:
duration和bounce参数在 SwiftUI、UIKit 和 Core Animation 中统一bounce范围是 -1.0 到 1.0- 0 表示 smooth,正值表示 bouncy,负值表示 flattened
Spring 模型类型
(18:57)Spring 类型可以独立使用:
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。
核心启发
-
全局替换 easeInOut
- 做什么:将 App 中所有
withAnimation和.animation()的默认调用改为显式 spring - 为什么值得做:iOS 17 默认已经是 smooth spring,但显式指定让意图更清晰,且可以微调参数
- 怎么开始:搜索项目中所有
.easeInOut和裸withAnimation,替换为.spring(duration:bounce:)
- 做什么:将 App 中所有
-
手势驱动的弹性交互
- 做什么:为拖拽、滑动等手势添加弹簧收尾动画
- 为什么值得做:弹簧自动继承手势速度,松手后的动画自然流畅,不需要手动计算速度
- 怎么开始:在
onEnded中用withAnimation(.spring)设置最终状态,SwiftUI 自动处理速度传递
-
可中断的动画系统
- 做什么:让动画可以被新动画打断并平滑过渡
- 为什么值得做:弹簧动画在被打断时会保留当前速度作为新动画的初始速度,用户体验连贯
- 怎么开始:确保使用 spring 动画,当新状态到来时直接调用
withAnimation(.spring)更新状态
-
品牌动效设计系统
- 做什么:为团队定义一套统一的动画参数规范
- 为什么值得做:duration + bounce 两个参数足够表达品牌调性(严肃 vs 活泼),且全团队容易理解
- 怎么开始:定义
extension Animation { static let brand = .spring(duration: 0.4, bounce: 0.1) },全 App 统一使用
-
Spring 驱动的数据可视化
- 做什么:用 Spring 模型计算值来驱动自定义图表或游戏物理
- 为什么值得做:可以复用苹果优化的弹簧数学,不需要自己实现物理模拟
- 怎么开始:创建
Spring实例,在TimelineView或游戏循环中调用spring.value(target:time:)获取当前值
关联 Session
- Explore SwiftUI animation — SwiftUI 动画系统完整原理
- Wind your way through advanced animations in SwiftUI — 关键帧和多阶段动画
- SwiftUI essentials — SwiftUI 基础概念
- Beyond scroll views — ScrollView 动画和过渡
评论
GitHub Issues · utterances