关于 axiom-swiftui-animation-ref
面向开发者的 SwiftUI 动画系统学习技能,涵盖插值、动画类型、事务和自定义动画。还介绍 iOS 26 的 `@Animatable` 宏,以及内置效果与自定义实现的取舍。
主要功能
- 01Animatable 与 VectorArithmetic 基础
- 02iOS 26 `@Animatable` 宏
- 03时间曲线和弹簧动画
- 04作用域动画与事务
- 05自定义动画算法
- 06自定义协议实现的性能指导
适用场景
- 01为自定义形状和布局添加动画
- 02选择弹簧或时间曲线效果
- 03将动画限定到特定视图属性
- 04动画化数值和进度显示
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swiftui-animation-ref安装后在 Claude Code 中输入/axiom-swiftui-animation-ref确认已加载。
详细介绍
SwiftUI 动画
概述
SwiftUI 动画系统全面指南,涵盖从基础概念到高级技巧。本指南介绍 Animatable 协议、iOS 26 的 @Animatable 宏、动画类型和 Transaction 系统。
核心原则 SwiftUI 中的动画是随时间进行的数学插值,由 VectorArithmetic 协议提供支持。理解这一基础,便能充分发挥 SwiftUI 声明式动画系统的强大功能。
系统要求
- iOS 13+:Animatable 协议、时序动画和弹簧动画
- iOS 17+:默认弹簧动画、作用域动画、PhaseAnimator、KeyframeAnimator
- iOS 18+:缩放转场、UIKit/AppKit 动画桥接
- iOS 26+:@Animatable 宏
第 1 部分:理解动画
什么是插值
动画是生成起始状态与结束状态之间中间值的过程。
示例:透明度动画
.opacity(0) → .opacity(1)
动画运行期间,SwiftUI 会计算中间值:
0.0 → 0.02 → 0.05 → 0.1 → 0.25 → 0.4 → 0.6 → 0.8 → 1.0
值的分布方式
- 由动画的时序曲线或速度函数决定
- 弹簧动画使用物理模拟
- 时序曲线使用贝塞尔曲线
- 每种动画类型的值计算方式各不相同
VectorArithmetic 协议
SwiftUI 要求动画数据遵循 VectorArithmetic,提供减法、缩放、加法和零值。这使 SwiftUI 能够在任意两个值之间进行插值。
内置遵循该协议的类型:CGFloat、Double、Float、Angle(1D)、CGPoint、CGSize(2D)、CGRect(4D)。
关键见解 向量运算抽象了维数差异。SwiftUI 使用单一泛型实现为所有这些类型添加动画。
为什么 Int 无法添加动画
Int 不遵循 VectorArithmetic——3 和 4 之间不存在小数中间值。SwiftUI 会直接跳变该值。
解决方案:使用 Float/Double,并显示为 Int:
@State private var count: Float = 0
// ...
Text("\(Int(count))")
.animation(.spring, value: count)
模型值与呈现值
从概念上讲,可动画属性有两个值:
模型值
- 由代码设置的目标值
- 状态变化时立即更新
- 在视图的 body 中编写的值
呈现值
- 当前正在渲染的插值结果
- 动画期间逐帧更新
- 用户实际看到的值
示例
.scaleEffect(selected ? 1.5 : 1.0)
当 selected 变为 true 时:
- 模型值:立即变为
1.5 - 呈现值:随时间插值,从
1.0 → 1.1 → 1.2 → 1.3 → 1.4 → 1.5
第 2 部分:Animatable 协议
概述
Animatable 协议允许视图定义哪些数据应进行插值,从而为其属性添加动画。
protocol Animatable {
associatedtype AnimatableData: VectorArithmetic
var animatableData: AnimatableData { get set }
}
SwiftUI 会为遵循此协议的任意视图构建可动画属性。
内置可动画视图
许多 SwiftUI 修饰符都遵循 Animatable:
视觉效果
.scaleEffect()— 为缩放变换添加动画.rotationEffect()— 为旋转添加动画.offset()— 为位置偏移添加动画.opacity()— 为透明度添加动画.blur()— 为模糊半径添加动画.shadow()— 为阴影属性添加动画
所有 Shape 类型
Circle、Rectangle、RoundedRectangleCapsule、Ellipse、Path- 自定义
Shape实现
用于多维数据的 AnimatablePair
为多个属性添加动画时,可使用 AnimatablePair 组合向量。例如,scaleEffect 通过 AnimatablePair<CGSize.AnimatableData, UnitPoint.AnimatableData> 将 CGSize(2D)和 UnitPoint(2D)组合成 4D 向量。使用 .first 和 .second 访问各个分量。@Animatable 宏(iOS 26+)完全免除了这些样板代码。
自定义 Animatable 遵循实现
适用场景
- 为自定义布局添加动画(例如 RadialLayout)
- 为自定义绘图代码添加动画
- 为影响形状路径的属性添加动画
示例:动画数字视图
struct AnimatableNumberView: View, Animatable {
var number: Double
var animatableData: Double {
get { number }
set { number = newValue }
}
var body: some View {
Text("\(Int(number))")
.font(.largeTitle)
}
}
// Usage
AnimatableNumberView(number: value)
.animation(.spring, value: value)
工作原理
number从 0 变为 100- SwiftUI 会为动画的每一帧调用
body - 每一帧都会获得新的
number值:0 → 5 → 15 → 30 → 55 → 80 → 100 - 文本会更新,以显示插值后的整数
性能警告
自定义 Animatable 一致性开销很大 —— SwiftUI 会在主线程上为每一帧调用 body。内置效果(.scaleEffect()、.opacity())会在主线程之外运行,且不会调用 body。只有在内置修饰符无法实现所需效果时,才使用自定义一致性(例如,创建一个会在每一帧重新定位子视图的自定义 Layout)。
第 3 部分:@Animatable 宏(iOS 26+)
概述
@Animatable 宏无需手动遵循 Animatable 协议,也无需编写样板代码。
iOS 26 之前,你必须:
- 手动遵循
Animatable - 编写
animatableDatagetter 和 setter - 为多个属性使用
AnimatablePair - 手动排除不可动画的属性
iOS 26+,只需添加 @Animatable:
@MainActor
@Animatable
struct MyView: View {
var scale: CGFloat
var opacity: Double
var body: some View {
// ...
}
}
该宏会自动:
- 生成
Animatable一致性 - 检查所有存储属性
- 根据遵循 VectorArithmetic 的属性创建
animatableData - 使用
AnimatablePair处理多维数据
前后对比
使用 @Animatable 宏之前
struct HikingRouteShape: Shape {
var startPoint: CGPoint
var endPoint: CGPoint
var elevation: Double
var drawingDirection: Bool // Don't want to animate this
// Tedious manual animatableData declaration
var animatableData: AnimatablePair<AnimatablePair<CGFloat, CGFloat>,
AnimatablePair<Double, AnimatablePair<CGFloat, CGFloat>>> {
get {
AnimatablePair(
AnimatablePair(startPoint.x, startPoint.y),
AnimatablePair(elevation, AnimatablePair(endPoint.x, endPoint.y))
)
}
set {
startPoint = CGPoint(x: newValue.first.first, y: newValue.first.second)
elevation = newValue.second.first
endPoint = CGPoint(x: newValue.second.second.first, y: newValue.second.second.second)
}
}
func path(in rect: CGRect) -> Path {
// Drawing code
}
}
使用 @Animatable 宏之后
@Animatable
struct HikingRouteShape: Shape {
var startPoint: CGPoint
var endPoint: CGPoint
var elevation: Double
@AnimatableIgnored
var drawingDirection: Bool // Excluded from animation
func path(in rect: CGRect) -> Path {
// Drawing code
}
}
代码行数:20 → 12(减少 40%)
@AnimatableIgnored
使用 @AnimatableIgnored 可将属性排除在动画之外。
适用场景
- 调试值 —— 仅用于开发的标志
- ID —— 不应参与动画的标识符
- 时间戳 —— 视图的创建或更新时间
- 内部状态 —— 非视觉用途的记录状态
- 非 VectorArithmetic 类型 —— 颜色、字符串、布尔值
示例
@MainActor
@Animatable
struct ProgressView: View {
var progress: Double // Animated
var totalItems: Int // Animated (if Float, not if Int)
@AnimatableIgnored
var title: String // Not animated
@AnimatableIgnored
var startTime: Date // Not animated
@AnimatableIgnored
var debugEnabled: Bool // Not animated
var body: some View {
VStack {
Text(title)
ProgressBar(value: progress)
if debugEnabled {
Text("Started: \(startTime.formatted())")
}
}
}
}
实际应用场景
@Animatable 适用于任何数值显示场景——股价、心率、分数、计时器、进度条:
@MainActor
@Animatable
struct AnimatedValueView: View {
var value: Double
var changePercent: Double
@AnimatableIgnored
var label: String
var body: some View {
VStack(alignment: .trailing) {
Text("\(value, format: .number.precision(.fractionLength(2)))")
.font(.title)
Text("\(changePercent > 0 ? "+" : "")\(changePercent, format: .percent)")
.foregroundStyle(changePercent > 0 ? .green : .red)
}
}
}
// Usage
AnimatedValueView(value: currentPrice, changePercent: 0.025, label: "Price")
.animation(.spring(duration: 0.8), value: currentPrice)
第 4 部分:动画类型
时间曲线动画
时间曲线动画使用贝塞尔曲线控制动画随时间变化的速度。
内置预设
.animation(.linear) // Constant speed
.animation(.easeIn) // Starts slow, ends fast
.animation(.easeOut) // Starts fast, ends slow
.animation(.easeInOut) // Slow start and end, fast middle
自定义时间曲线
let customCurve = UnitCurve(
startControlPoint: CGPoint(x: 0.2, y: 0),
endControlPoint: CGPoint(x: 0.8, y: 1)
)
.animation(.timingCurve(customCurve, duration: 0.5))
时长
所有时间曲线动画都接受可选的时长:
.animation(.easeInOut(duration: 0.3))
.animation(.linear(duration: 1.0))
默认值:0.35 秒
弹簧动画
弹簧动画使用物理模拟,营造自然、流畅的运动效果。
内置预设
.animation(.smooth) // No bounce (default since iOS 17)
.animation(.snappy) // Small amount of bounce
.animation(.bouncy) // Larger amount of bounce
自定义弹簧
.animation(.spring(duration: 0.6, bounce: 0.3))
参数
duration—— 感知到的动画时长bounce—— 弹跳程度(0 = 不弹跳,1 = 弹跳幅度很大)
比传统弹簧参数(质量、刚度、阻尼)直观得多。
高阶动画
修改基础动画以创建复杂效果。
延迟
.animation(.spring.delay(0.5))
等待 0.5 秒后开始动画。
重复
.animation(.easeInOut.repeatCount(3, autoreverses: true))
.animation(.linear.repeatForever(autoreverses: false))
重复播放动画多次或无限次。
速度
.animation(.spring.speed(2.0)) // 2x faster
.animation(.spring.speed(0.5)) // 2x slower
按倍数调整动画速度。
默认动画变更(iOS 17+)
iOS 17 之前
withAnimation {
// Used timing curve by default
}
iOS 17+
withAnimation {
// Uses .smooth spring by default
}
变更原因:弹簧动画感觉更自然,并且在动画被中断时能保留速度。
建议:采用弹簧动画。它们能让 UI 响应更灵敏、质感更出色。
第 5 部分:事务系统
withAnimation
触发动画最常见的方式。
Button("Scale Up") {
withAnimation(.spring) {
scale = 1.5
}
}
工作原理
withAnimation开启一个事务- 在事务字典中设置动画
- 执行闭包(状态发生变化)
- 事务沿视图层级向下传递
- 可动画属性会检查是否存在动画,并进行插值
显式动画
withAnimation(.spring(duration: 0.6, bounce: 0.4)) {
isExpanded.toggle()
}
无动画
withAnimation(nil) {
// Changes happen immediately, no animation
resetState()
}
animation() 视图修饰符
将动画应用于视图中的特定值。
基本用法
Circle()
.fill(isActive ? .blue : .gray)
.animation(.spring, value: isActive)
工作原理:只有在 isActive 发生变化时,动画才会生效。其他状态变化不会触发此动画。
同一视图上的多个动画
Circle()
.scaleEffect(scale)
.animation(.bouncy, value: scale)
.opacity(opacity)
.animation(.easeInOut, value: opacity)
为不同属性设置不同的动画。
作用域动画(iOS 17+)
将动画限定到特定的可动画属性。
旧方法的问题
struct AvatarView: View {
var selected: Bool
var body: some View {
Image("avatar")
.scaleEffect(selected ? 1.5 : 1.0)
.animation(.spring, value: selected)
// ⚠️ If image also changes when selected changes,
// image transition gets animated too (accidental)
}
}
解决方案:作用域动画
struct AvatarView: View {
var selected: Bool
var body: some View {
Image("avatar")
.animation(.spring, value: selected) {
$0.scaleEffect(selected ? 1.5 : 1.0)
}
// ✅ Only scaleEffect animates, image transition doesn't
}
}
工作原理
- 动画仅应用于闭包中的属性
- 其他属性不受影响
- 防止意外触发动画
自定义 Transaction 键
定义自定义 TransactionKey 类型,以便通过事务系统传递上下文。使用 withTransaction 设置值,并使用 .transaction 修饰符读取值。这样可以根据状态变化的触发方式(轻点或编程触发)应用不同的动画。
第 6 部分:高级主题
CustomAnimation 协议
实现自己的动画算法。
protocol CustomAnimation {
// Calculate current value
func animate<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: inout AnimationContext<V>
) -> V?
// Optional: Should this animation merge with previous?
func shouldMerge<V>(previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext<V>) -> Bool
// Optional: Current velocity
func velocity<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: AnimationContext<V>
) -> V?
}
示例:线性时间曲线
struct LinearAnimation: CustomAnimation {
let duration: TimeInterval
func animate<V: VectorArithmetic>(
value: V, // Delta vector: target - current
time: TimeInterval,
context: inout AnimationContext<V>
) -> V? {
if time >= duration { return nil }
return value.scaled(by: time / duration)
}
}
关键理解:value 是增量向量(目标值 - 当前值),而不是目标值。完成后返回 nil。SwiftUI 会自动将缩放后的增量加到当前值上。
动画合并行为
如果在上一个动画结束前启动新动画,会发生什么?
时间曲线动画(默认:不合并)
func shouldMerge(...) -> Bool {
return false // Default implementation
}
行为:两个动画同时运行,结果以加法方式合并。
示例
- 第一次轻点:将 1.0 动画到 1.5(正在运行)
- 第二次轻点(结束前):将 1.5 动画到 1.0
- 结果:两个动画同时运行,数值合并
弹簧动画(合并并重新设定目标)
func shouldMerge(...) -> Bool {
return true // Springs override this
}
行为:新动画会纳入上一个动画的状态,并保留速度。
示例
- 第一次轻点:以速度 V 将 1.0 动画到 1.5
- 第二次轻点(结束前):将目标改为 1.0,同时保留当前速度 V
- 结果:平滑过渡,速度不会突变
弹簧动画感觉更自然的原因:中断时会保留动量。
第 7 部分:多步动画(iOS 17+)
PhaseAnimator
循环播放一系列阶段,并在每个阶段应用不同的修饰符。每次阶段转换都会单独进行动画处理。
PhaseAnimator([false, true]) { phase in
Image(systemName: "star.fill")
.scaleEffect(phase ? 1.5 : 1.0)
.opacity(phase ? 1.0 : 0.5)
.rotationEffect(.degrees(phase ? 360 : 0))
} animation: { phase in
phase ? .spring(duration: 0.8, bounce: 0.3) : .easeInOut(duration: 0.4)
}
工作原理:从第一个阶段开始,动画过渡到第二个阶段,然后循环播放。animation 闭包会返回用于过渡到该阶段的动画。阶段可以是任意 Equatable 类型——对于复杂的多步序列,请使用枚举:
enum PulsePhase: CaseIterable { case idle, expand, contract }
PhaseAnimator(PulsePhase.allCases) { phase in
Circle()
.scaleEffect(phase == .expand ? 1.3 : phase == .contract ? 0.9 : 1.0)
}
触发方式:添加 trigger 参数,仅在值变化时运行动画(而不是持续循环)。
KeyframeAnimator
为每个属性提供关键帧轨道,实现精确的、基于时间线的动画。比 PhaseAnimator 提供更精细的控制。
struct AnimationValues {
var scale: Double = 1.0
var rotation: Angle = .zero
var yOffset: Double = 0
}
KeyframeAnimator(initialValue: AnimationValues()) { values in
Image(systemName: "heart.fill")
.scaleEffect(values.scale)
.rotationEffect(values.rotation)
.offset(y: values.yOffset)
} keyframes: { _ in
KeyframeTrack(\.scale) {
SpringKeyframe(1.5, duration: 0.3)
SpringKeyframe(1.0, duration: 0.3)
}
KeyframeTrack(\.rotation) {
LinearKeyframe(.degrees(15), duration: 0.15)
LinearKeyframe(.degrees(-15), duration: 0.3)
LinearKeyframe(.zero, duration: 0.15)
}
KeyframeTrack(\.yOffset) {
CubicKeyframe(-20, duration: 0.3)
CubicKeyframe(0, duration: 0.3)
}
}
关键帧类型:LinearKeyframe(恒定速度)、SpringKeyframe(弹簧物理效果)、CubicKeyframe(贝塞尔曲线)、MoveKeyframe(瞬间跳变,不进行插值)。
与 PhaseAnimator 的区别:简单的状态循环使用 PhaseAnimator。不同属性需要独立计时时使用 KeyframeAnimator。
.transition()
定义视图插入视图层级或从中移除时的动画方式。
if showDetail {
DetailView()
.transition(.slide) // Slide in/out
.transition(.scale.combined(with: .opacity)) // Combine transitions
.transition(.move(edge: .bottom)) // Move from edge
.transition(.asymmetric( // Different in/out
insertion: .scale.combined(with: .opacity),
removal: .opacity
))
}
需要动画上下文——使用 withAnimation 包裹状态变化,或使用 .animation() 修饰符。没有动画时,视图会立即出现或消失。
matchedGeometryEffect
在视图层级中的两个位置之间平滑地动画过渡视图的框架。常用于主视觉过渡和共享元素动画。
@Namespace private var animation
// Source
if !isExpanded {
RoundedRectangle(cornerRadius: 10)
.matchedGeometryEffect(id: "card", in: animation)
.frame(width: 100, height: 100)
}
// Destination
if isExpanded {
RoundedRectangle(cornerRadius: 20)
.matchedGeometryEffect(id: "card", in: animation)
.frame(width: 300, height: 400)
}
关键规则:相同的 id + 相同的 Namespace = 匹配的一对视图。同一时间,具有给定 ID 的视图中只能有一个设置为 isSource: true(默认值)。使用 withAnimation 包裹状态变化,以实现平滑插值。
contentTransition
为视图中的文本和符号内容变化添加动画效果(iOS 16+)。
Text(value, format: .number)
.contentTransition(.numericText(countsDown: value < previous))
Image(systemName: isFavorite ? "heart.fill" : "heart")
.contentTransition(.symbolEffect(.replace))
第 8 部分:缩放过渡(iOS 18+)
概述
iOS 18 引入了缩放转场,点击的单元格会变形为进入的视图。此转场始终可交互——用户可以在转场开始期间或之后抓住视图并拖动。
主要优势 在从大型单元格进行转场的应用部分中,缩放转场会让相同的 UI 元素在转场过程中始终显示在屏幕上,从而增强视觉连贯性。
SwiftUI 实现
采用缩放转场分为两步:
第 1 步:在目标视图上声明转场样式
NavigationLink {
BraceletEditor(bracelet)
.navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
} label: {
BraceletPreview(bracelet)
}
第 2 步:标记源视图
BraceletPreview(bracelet)
.matchedTransitionSource(id: bracelet.id, in: namespace)
完整示例
struct BraceletListView: View {
@Namespace private var braceletList
let bracelets: [Bracelet]
var body: some View {
NavigationStack {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 150))]) {
ForEach(bracelets) { bracelet in
NavigationLink {
BraceletEditor(bracelet: bracelet)
.navigationTransition(
.zoom(sourceID: bracelet.id, in: braceletList)
)
} label: {
BraceletPreview(bracelet: bracelet)
}
.matchedTransitionSource(id: bracelet.id, in: braceletList)
}
}
}
}
}
}
UIKit 实现
在被推送的视图控制器上设置 preferredTransition = .zoom { context in ... }。闭包会返回源视图,并且在放大和缩小时都会被调用——请捕获稳定的标识符(模型对象),而不是直接捕获视图。
展示方式
缩放转场也适用于 fullScreenCover 和 sheet:
.fullScreenCover(item: $selectedBracelet) { bracelet in
BraceletEditor(bracelet: bracelet)
.navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
}
设置源视图样式
.matchedTransitionSource(id: bracelet.id, in: namespace) { source in
source.cornerRadius(8.0).shadow(radius: 4)
}
流畅的转场生命周期
推送转场无法取消——被中断时会转换为弹出转场。视图控制器始终会到达 Appeared 状态。不要针对重叠转场添加保护逻辑;让系统处理即可。
第 9 部分:UIKit/AppKit 动画桥接(iOS 18+)
概述
iOS 18 支持使用 SwiftUI Animation 类型为 UIKit 和 AppKit 视图添加动画。这让你可以使用完整的 SwiftUI 动画套件,包括自定义动画。
基本用法
// Old way
UIView.animate(withDuration: 0.5, delay: 0,
usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5) {
bead.center = endOfBracelet
}
// New way: Use SwiftUI Animation type
UIView.animate(.spring(duration: 0.5)) {
bead.center = endOfBracelet
}
所有 SwiftUI 动画都可用:.linear、.easeIn/Out、.spring、.smooth、.snappy、.bouncy、.repeatForever(),以及自定义动画。
架构说明:与旧版 UIKit API 不同,不会生成 CAAnimation——而是直接为呈现值添加动画。
第 10 部分:UIViewRepresentable 动画桥接(iOS 18+)
问题
将 UIKit 视图包装在 SwiftUI 中时,动画不会自动桥接:
struct BeadBoxWrapper: UIViewRepresentable {
@Binding var isOpen: Bool
func updateUIView(_ box: BeadBox, context: Context) {
// ❌ Animation on binding doesn't affect UIKit
box.lid.center.y = isOpen ? -100 : 100
}
}
// Usage
BeadBoxWrapper(isOpen: $isOpen)
.animation(.spring, value: isOpen) // No effect on UIKit view
解决方案:context.animate()
使用 context.animate() 桥接 SwiftUI 动画:
struct BeadBoxWrapper: UIViewRepresentable {
@Binding var isOpen: Bool
func makeUIView(context: Context) -> BeadBox {
BeadBox()
}
func updateUIView(_ box: BeadBox, context: Context) {
// ✅ Bridges animation from Transaction to UIKit
context.animate {
box.lid.center.y = isOpen ? -100 : 100
}
}
}
工作原理
- SwiftUI 将动画信息存储在当前
Transaction中 context.animate()读取Transaction的动画- 将该动画应用于闭包中的 UIView 更改
- 如果
Transaction中没有动画,更改会立即生效(无动画)
关键行为
context.animate {
// Changes here
} completion: {
// Called when animation completes
// If not animated, called immediately inline
}
无论是否启用动画都适用——始终使用此模式都是安全的。
完美同步
在 SwiftUI 视图和 UIView 之间运行的单个动画能够完美同步。这让混合层级结构也能流畅衔接。
第 11 部分:手势驱动的动画(iOS 18+)
自动保留速度
SwiftUI 动画会通过动画合并自动保留速度——无需手动计算速度:
// UIKit with SwiftUI animations
func handlePan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .changed:
UIView.animate(.interactiveSpring) {
bead.center = gesture.location(in: view)
}
case .ended:
UIView.animate(.spring) { // Inherits velocity automatically
bead.center = endOfBracelet
}
default: break
}
}
// Pure SwiftUI equivalent
DragGesture()
.onChanged { value in
withAnimation(.interactiveSpring) { position = value.location }
}
.onEnded { _ in
withAnimation(.spring) { position = targetPosition }
}
每个 .interactiveSpring 都会重新设定前一个动画的目标,而最终的 .spring 会继承累积的速度,实现平滑减速。
故障排除
属性未添加动画
依次检查:
- 类型是否遵循 VectorArithmetic? —
Int无法添加动画;请使用Double/Float - 是否存在动画修饰符? — 需要
.animation(.spring, value: x)或withAnimation - 跟踪的值是否正确? — 应使用
.animation(.spring, value: progress),而不是.animation(.spring, value: title) - 视图是否遵循 Animatable? — 自定义视图需要使用
@Animatable(iOS 26+)或手动实现animatableData
动画卡顿
自定义的 Animatable 遵循会在主线程上每帧调用一次 body。尽可能使用内置效果(.opacity()、.scaleEffect())——它们会在主线程之外运行。对于复杂情况,请使用 Instruments 进行性能分析。
意外的动画合并
弹簧动画默认会合并,并保留速度。如果不希望出现合并行为,请使用计时曲线动画(.easeInOut)。请参阅上文的动画合并行为部分。
资源
WWDC:2023-10156、2023-10157、2023-10158、2024-10145、2025-256
文档:/swiftui/animatable、/swiftui/animation、/swiftui/vectorarithmetic、/swiftui/transaction、/swiftui/view/navigationtransition(:)、/swiftui/view/matchedtransitionsource(id:in:configuration:)、/uikit/uiview/animate(:changes:completion:)
技能:axiom-swiftui-26-ref、axiom-swiftui-nav-ref、axiom-swiftui-performance、axiom-swiftui-debugging、axiom-sf-symbols-ref