axiom-swiftui-animation-ref
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI Animation
SwiftUI 动画
Overview
概述
Comprehensive guide to SwiftUI's animation system, from foundational concepts to advanced techniques. This skill covers the Animatable protocol, the iOS 26 @Animatable macro, animation types, and the Transaction system.
Core principle Animation in SwiftUI is mathematical interpolation over time, powered by the VectorArithmetic protocol. Understanding this foundation unlocks the full power of SwiftUI's declarative animation system.
关于SwiftUI动画系统的全面指南,从基础概念到高级技巧。本内容涵盖Animatable协议、iOS 26的@Animatable宏、动画类型以及Transaction系统。
核心原则 SwiftUI中的动画是基于时间的数学插值,由VectorArithmetic协议提供支持。理解这一基础能解锁SwiftUI声明式动画系统的全部能力。
When to Use This Skill
何时使用本内容
- Implementing custom animated views or shapes
- Understanding why a property doesn't animate (Int vs Float/Double)
- Choosing between spring and timing curve animations
- Using the @Animatable macro (iOS 26+)
- Conforming views to the Animatable protocol
- Debugging animation merging behavior
- Optimizing animation performance
- Creating multi-step or complex animations
- Understanding model vs presentation values
- Implementing custom animation algorithms
- Adding zoom transitions for navigation/presentation (iOS 18+)
- Animating UIKit/AppKit views with SwiftUI animations (iOS 18+)
- Bridging animations through UIViewRepresentable (iOS 18+)
- Building gesture-driven animations with velocity preservation
- 实现自定义动画视图或形状
- 理解为何某个属性无法动画(Int与Float/Double的区别)
- 在弹簧动画和时间曲线动画之间做选择
- 使用@Animatable宏(iOS 26+)
- 让视图遵循Animatable协议
- 调试动画合并行为
- 优化动画性能
- 创建多步骤或复杂动画
- 理解模型值与展示值的区别
- 实现自定义动画算法
- 为导航/转场添加缩放过渡(iOS 18+)
- 使用SwiftUI动画为UIKit/AppKit视图添加动画(iOS 18+)
- 通过UIViewRepresentable桥接动画(iOS 18+)
- 构建保留速度的手势驱动动画
System Requirements
系统要求
iOS 13+ for Animatable protocol
iOS 13+ 支持Animatable协议
iOS 17+ for default spring animations, scoped animations
iOS 17+ 支持默认弹簧动画、范围动画
iOS 18+ for zoom transitions, UIKit/AppKit animation bridging
iOS 18+ 支持缩放过渡、UIKit/AppKit动画桥接
iOS 26+ for @Animatable macro
iOS 26+ 支持@Animatable宏
Part 1: Understanding Animation
第一部分:理解动画
What Is Interpolation
什么是插值
Animation is the process of generating intermediate values between a start and end state.
动画是在起始状态和结束状态之间生成中间值的过程。
Example: Opacity animation
示例:透明度动画
swift
.opacity(0) → .opacity(1)While this animation runs, SwiftUI computes intermediate values:
0.0 → 0.02 → 0.05 → 0.1 → 0.25 → 0.4 → 0.6 → 0.8 → 1.0How values are distributed
- Determined by the animation's timing curve or velocity function
- Spring animations use physics simulation
- Timing curves use bezier curves
- Each animation type calculates values differently
swift
.opacity(0) → .opacity(1)在动画运行时,SwiftUI会计算中间值:
0.0 → 0.02 → 0.05 → 0.1 → 0.25 → 0.4 → 0.6 → 0.8 → 1.0值的分布方式
- 由动画的时间曲线或速度函数决定
- 弹簧动画使用物理模拟
- 时间曲线使用贝塞尔曲线
- 每种动画类型的数值计算方式不同
VectorArithmetic Protocol
VectorArithmetic协议
SwiftUI requires animated data to conform to , which provides:
VectorArithmeticswift
protocol VectorArithmetic {
// Compute difference between two values
static func - (lhs: Self, rhs: Self) -> Self
// Scale values
static func * (lhs: Self, rhs: Double) -> Self
// Add values
static func + (lhs: Self, rhs: Self) -> Self
// Zero value
static var zero: Self { get }
}Built-in conforming types
- 1-dimensional: ,
CGFloat,Double,FloatAngle - 2-dimensional: ,
CGPointCGSize - 4-dimensional:
CGRect
Key insight Vector arithmetic abstracts over the dimensionality of animated data. SwiftUI can animate all these types with a single generic implementation.
SwiftUI要求可动画的数据遵循协议,该协议提供以下能力:
VectorArithmeticswift
protocol VectorArithmetic {
// 计算两个值之间的差值
static func - (lhs: Self, rhs: Self) -> Self
// 缩放值
static func * (lhs: Self, rhs: Double) -> Self
// 相加值
static func + (lhs: Self, rhs: Self) -> Self
// 零值
static var zero: Self { get }
}内置遵循类型
- 一维类型:,
CGFloat,Double,FloatAngle - 二维类型:,
CGPointCGSize - 四维类型:
CGRect
关键要点 向量运算抽象了可动画数据的维度。SwiftUI可以通过单一的泛型实现为所有这些类型添加动画。
Why Int Can't Be Animated
为何Int无法动画
Int- No fractional intermediate values — There is no "3.5" between 3 and 4
- Not continuous — Integers are discrete values
- Scaling doesn't make sense — What does 5 × 0.5 mean for an integer?
Int- 没有分数中间值——3和4之间不存在“3.5”
- 非连续——整数是离散值
- 缩放无意义——整数5×0.5是什么意思?
What happens when you try
尝试动画Int的结果
swift
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
Text("\(count)")
.animation(.spring, value: count)
}
}Result: SwiftUI simply replaces the old text with the new one. No interpolation occurs.
swift
struct CounterView: View {
@State private var count: Int = 0
var body: some View {
Text("\(count)")
.animation(.spring, value: count)
}
}结果:SwiftUI只是直接用新文本替换旧文本,不会进行插值。
Solution: Use Float or Double
解决方案:使用Float或Double
swift
struct AnimatedCounterView: View {
@State private var count: Float = 0
var body: some View {
Text("\(Int(count))")
.animation(.spring, value: count)
}
}Result: SwiftUI interpolates , and you display the rounded integer at each frame.
0.0 → ... → 100.0swift
struct AnimatedCounterView: View {
@State private var count: Float = 0
var body: some View {
Text("\(Int(count))")
.animation(.spring, value: count)
}
}结果:SwiftUI会对进行插值,你可以在每一帧显示取整后的整数。
0.0 → ... → 100.0Model vs Presentation Values
模型值与展示值
Animatable attributes conceptually have two values:
可动画属性在概念上有两个值:
Model Value
模型值
- The target value set by your code
- Updated immediately when state changes
- What you write in your view's body
- 由代码设置的目标值
- 状态变化时立即更新
- 你在视图body中编写的值
Presentation Value
展示值
- The current interpolated value being rendered
- Updates frame-by-frame during animation
- What the user actually sees
Example
swift
.scaleEffect(selected ? 1.5 : 1.0)When becomes :
selectedtrue- Model value: Immediately becomes
1.5 - Presentation value: Interpolates over time
1.0 → 1.1 → 1.2 → 1.3 → 1.4 → 1.5
- 当前正在渲染的插值后的数值
- 动画过程中逐帧更新
- 用户实际看到的值
示例
swift
.scaleEffect(selected ? 1.5 : 1.0)当变为时:
selectedtrue- 模型值:立即变为
1.5 - 展示值:在一段时间内从进行插值
1.0 → 1.1 → 1.2 → 1.3 → 1.4 → 1.5
Part 2: Animatable Protocol
第二部分:Animatable协议
Overview
概述
The protocol allows views to animate their properties by defining which data should be interpolated.
Animatableswift
protocol Animatable {
associatedtype AnimatableData: VectorArithmetic
var animatableData: AnimatableData { get set }
}SwiftUI builds an animatable attribute for any view conforming to this protocol.
Animatableswift
protocol Animatable {
associatedtype AnimatableData: VectorArithmetic
var animatableData: AnimatableData { get set }
}SwiftUI会为任何遵循该协议的视图构建可动画属性。
Built-in Animatable Views
内置可动画视图
Many SwiftUI modifiers conform to Animatable:
许多SwiftUI修饰符都遵循Animatable:
Visual Effects
视觉效果
- — Animates scale transform
.scaleEffect() - — Animates rotation
.rotationEffect() - — Animates position offset
.offset() - — Animates transparency
.opacity() - — Animates blur radius
.blur() - — Animates shadow properties
.shadow()
- —— 为缩放变换添加动画
.scaleEffect() - —— 为旋转变换添加动画
.rotationEffect() - —— 为位置偏移添加动画
.offset() - —— 为透明度添加动画
.opacity() - —— 为模糊半径添加动画
.blur() - —— 为阴影属性添加动画
.shadow()
All Shape types
所有Shape类型
- ,
Circle,RectangleRoundedRectangle - ,
Capsule,EllipsePath - Custom implementations
Shape
- ,
Circle,RectangleRoundedRectangle - ,
Capsule,EllipsePath - 自定义实现
Shape
AnimatablePair for Multi-Dimensional Data
用于多维数据的AnimatablePair
When animating multiple properties, use to combine vectors.
AnimatablePair当为多个属性添加动画时,使用来组合向量。
AnimatablePairExample: scaleEffect implementation
示例:scaleEffect实现
swift
struct ScaleEffectModifier: ViewModifier, Animatable {
var scale: CGSize
var anchor: UnitPoint
// Combine two 2D vectors into one 4D vector
var animatableData: AnimatablePair<CGSize.AnimatableData, UnitPoint.AnimatableData> {
get {
AnimatablePair(scale.animatableData, anchor.animatableData)
}
set {
scale.animatableData = newValue.first
anchor.animatableData = newValue.second
}
}
func body(content: Content) -> some View {
content.scaleEffect(scale, anchor: anchor)
}
}How it works
- is 2-dimensional (width, height)
CGSize - is 2-dimensional (x, y)
UnitPoint - fuses them into a 4-dimensional vector
AnimatablePair - SwiftUI interpolates all 4 values together
swift
struct ScaleEffectModifier: ViewModifier, Animatable {
var scale: CGSize
var anchor: UnitPoint
// 将两个二维向量组合成一个四维向量
var animatableData: AnimatablePair<CGSize.AnimatableData, UnitPoint.AnimatableData> {
get {
AnimatablePair(scale.animatableData, anchor.animatableData)
}
set {
scale.animatableData = newValue.first
anchor.animatableData = newValue.second
}
}
func body(content: Content) -> some View {
content.scaleEffect(scale, anchor: anchor)
}
}工作原理
- 是二维的(宽度、高度)
CGSize - 是二维的(x、y)
UnitPoint - 将它们融合成一个四维向量
AnimatablePair - SwiftUI会同时对这4个值进行插值
Custom Animatable Conformance
自定义Animatable协议遵循
When to use
适用场景
- Animating custom layout (like RadialLayout)
- Animating custom drawing code
- Animating properties that affect shape paths
- 为自定义布局添加动画(如RadialLayout)
- 为自定义绘制代码添加动画
- 为影响形状路径的属性添加动画
Example: Animated number view
示例:可动画数字视图
swift
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)How it works
- changes from 0 to 100
number - SwiftUI calls for every frame of the animation
body - Each frame gets a new value: 0 → 5 → 15 → 30 → 55 → 80 → 100
number - Text updates to show the interpolated integer
swift
struct AnimatableNumberView: View, Animatable {
var number: Double
var animatableData: Double {
get { number }
set { number = newValue }
}
var body: some View {
Text("\(Int(number))")
.font(.largeTitle)
}
}
// 使用方式
AnimatableNumberView(number: value)
.animation(.spring, value: value)工作原理
- 从0变为100
number - SwiftUI会在动画的每一帧调用
body - 每一帧都会得到一个新的值:0 → 5 → 15 → 30 → 55 → 80 → 100
number - 文本会更新以显示插值后的整数
Performance Warning
性能警告
Custom Animatable conformance can be expensive.
When you conform a view to Animatable:
- SwiftUI calls your view's for every frame of the animation
body - Layout is rerun every frame
- This happens on the main thread
Built-in animatable effects (like , ) are much more efficient:
.scaleEffect().opacity()- They run off the main thread
- They don't call your view's body
- They update only the rendering layer
Guideline
- Use built-in effects whenever possible
- Only use custom Animatable conformance if you can't achieve the effect with built-in modifiers
- Profile with Instruments if you have performance issues
自定义Animatable协议遵循可能会消耗较多性能。
当你让视图遵循Animatable时:
- SwiftUI会在动画的每一帧调用视图的
body - 布局会逐帧重新计算
- 这些操作都在主线程上执行
内置可动画效果(如、)的效率要高得多:
.scaleEffect().opacity()- 它们在主线程外运行
- 不会调用你的视图body
- 仅更新渲染层
指导原则
- 尽可能使用内置效果
- 只有当无法通过内置修饰符实现所需效果时,才使用自定义Animatable协议遵循
- 如果遇到性能问题,使用Instruments进行分析
Example: Circular layout animation
示例:环形布局动画
swift
// This is expensive but necessary for animating along a circular path
@Animatable
struct RadialLayout: Layout {
var offsetAngle: Angle
var animatableData: Angle.AnimatableData {
get { offsetAngle.animatableData }
set { offsetAngle.animatableData = newValue }
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let radius = min(bounds.width, bounds.height) / 2
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let angleStep = Angle.degrees(360.0 / Double(subviews.count))
for (index, subview) in subviews.enumerated() {
let angle = offsetAngle + angleStep * Double(index)
let x = center.x + radius * cos(angle.radians)
let y = center.y + radius * sin(angle.radians)
subview.place(at: CGPoint(x: x, y: y), anchor: .center, proposal: .unspecified)
}
}
}Why necessary: Animating requires recalculating positions every frame. No built-in modifier can do this.
offsetAngleswift
// 这种实现虽然性能消耗大,但却是为环形路径添加动画的必要方式
@Animatable
struct RadialLayout: Layout {
var offsetAngle: Angle
var animatableData: Angle.AnimatableData {
get { offsetAngle.animatableData }
set { offsetAngle.animatableData = newValue }
}
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
let radius = min(bounds.width, bounds.height) / 2
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let angleStep = Angle.degrees(360.0 / Double(subviews.count))
for (index, subview) in subviews.enumerated() {
let angle = offsetAngle + angleStep * Double(index)
let x = center.x + radius * cos(angle.radians)
let y = center.y + radius * sin(angle.radians)
subview.place(at: CGPoint(x: x, y: y), anchor: .center, proposal: .unspecified)
}
}
}必要性:为添加动画需要逐帧重新计算位置。没有内置修饰符可以实现这一点。
offsetAnglePart 3: @Animatable Macro (iOS 26+)
第三部分:@Animatable宏(iOS 26+)
Overview
概述
The macro eliminates the boilerplate of manually conforming to the Animatable protocol.
@AnimatableBefore iOS 26, you had to:
- Manually conform to
Animatable - Write getter and setter
animatableData - Use for multiple properties
AnimatablePair - Exclude non-animatable properties manually
iOS 26+, you just add :
@Animatableswift
@MainActor
@Animatable
struct MyView: View {
var scale: CGFloat
var opacity: Double
var body: some View {
// ...
}
}The macro automatically:
- Generates conformance
Animatable - Inspects all stored properties
- Creates from VectorArithmetic-conforming properties
animatableData - Handles multi-dimensional data with
AnimatablePair
@AnimatableiOS 26之前,你需要:
- 手动遵循协议
Animatable - 编写的getter和setter
animatableData - 对多个属性使用
AnimatablePair - 手动排除不可动画的属性
iOS 26+,你只需添加:
@Animatableswift
@MainActor
@Animatable
struct MyView: View {
var scale: CGFloat
var opacity: Double
var body: some View {
// ...
}
}该宏会自动:
- 生成协议遵循代码
Animatable - 检查所有存储属性
- 从遵循VectorArithmetic的属性创建
animatableData - 使用处理多维数据
AnimatablePair
Before/After Comparison
前后对比
Before @Animatable macro
@Animatable宏之前的实现
swift
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
}
}swift
struct HikingRouteShape: Shape {
var startPoint: CGPoint
var endPoint: CGPoint
var elevation: Double
var drawingDirection: Bool // 不需要为这个属性添加动画
// 繁琐的手动animatableData声明
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 {
// 绘制代码
}
}After @Animatable macro
@Animatable宏之后的实现
swift
@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
}
}Lines of code: 20 → 12 (40% reduction)
swift
@Animatable
struct HikingRouteShape: Shape {
var startPoint: CGPoint
var endPoint: CGPoint
var elevation: Double
@AnimatableIgnored
var drawingDirection: Bool // 排除在动画之外
func path(in rect: CGRect) -> Path {
// 绘制代码
}
}代码行数:20行 → 12行(减少40%)
@AnimatableIgnored
@AnimatableIgnored
Use to exclude properties from animation.
@AnimatableIgnored使用来排除不需要动画的属性。
@AnimatableIgnoredWhen to use
适用场景
- Debug values — Flags for development only
- IDs — Identifiers that shouldn't animate
- Timestamps — When the view was created/updated
- Internal state — Non-visual bookkeeping
- Non-VectorArithmetic types — Colors, strings, booleans
- 调试值——仅用于开发的标志
- ID——不应动画的标识符
- 时间戳——视图创建/更新的时间
- 内部状态——非视觉的记录数据
- 非VectorArithmetic类型——颜色、字符串、布尔值
Example
示例
swift
@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())")
}
}
}
}swift
@MainActor
@Animatable
struct ProgressView: View {
var progress: Double // 可动画
var totalItems: Int // 如果是Float则可动画,Int不可
@AnimatableIgnored
var title: String // 不可动画
@AnimatableIgnored
var startTime: Date // 不可动画
@AnimatableIgnored
var debugEnabled: Bool // 不可动画
var body: some View {
VStack {
Text(title)
ProgressBar(value: progress)
if debugEnabled {
Text("Started: \(startTime.formatted())")
}
}
}
}Real-World Use Cases
实际应用场景
Numeric animations are extremely common across app categories:
数值动画在各类应用中极为常见:
Fintech Apps
金融科技应用
swift
@MainActor
@Animatable
struct StockPriceView: View {
var price: Double
var changePercent: Double
var body: some View {
VStack(alignment: .trailing) {
Text("$\(price, format: .number.precision(.fractionLength(2)))")
.font(.title)
Text("\(changePercent > 0 ? "+" : "")\(changePercent, format: .percent)")
.foregroundColor(changePercent > 0 ? .green : .red)
}
}
}Use case: Animate stock price changes, portfolio value, account balance transitions
swift
@MainActor
@Animatable
struct StockPriceView: View {
var price: Double
var changePercent: Double
var body: some View {
VStack(alignment: .trailing) {
Text("$\(price, format: .number.precision(.fractionLength(2)))")
.font(.title)
Text("\(changePercent > 0 ? "+" : "")\(changePercent, format: .percent)")
.foregroundColor(changePercent > 0 ? .green : .red)
}
}
}应用场景:为股价变化、投资组合价值、账户余额转换添加动画
Health & Fitness
健康与健身应用
swift
@MainActor
@Animatable
struct HeartRateView: View {
var bpm: Double
@AnimatableIgnored
var timestamp: Date
var body: some View {
VStack {
Text("\(Int(bpm))")
.font(.system(size: 60, weight: .bold))
Text("BPM")
.font(.caption)
.foregroundColor(.secondary)
}
}
}Use case: Heart rate indicators, step counters, calorie calculations, distance traveled
swift
@MainActor
@Animatable
struct HeartRateView: View {
var bpm: Double
@AnimatableIgnored
var timestamp: Date
var body: some View {
VStack {
Text("\(Int(bpm))")
.font(.system(size: 60, weight: .bold))
Text("BPM")
.font(.caption)
.foregroundColor(.secondary)
}
}
}应用场景:心率指示器、计步器、卡路里计算、行进距离
Games
游戏应用
swift
@MainActor
@Animatable
struct ScoreView: View {
var score: Float
var multiplier: Float
var body: some View {
HStack {
Text("\(Int(score))")
.font(.largeTitle)
Text("×\(multiplier, format: .number.precision(.fractionLength(1)))")
.font(.title2)
.foregroundColor(.orange)
}
}
}Use case: Score animations, XP transitions, level progress, combo multipliers
swift
@MainActor
@Animatable
struct ScoreView: View {
var score: Float
var multiplier: Float
var body: some View {
HStack {
Text("\(Int(score))")
.font(.largeTitle)
Text("×\(multiplier, format: .number.precision(.fractionLength(1)))")
.font(.title2)
.foregroundColor(.orange)
}
}
}应用场景:分数动画、经验值转换、关卡进度、 combo 倍数
Productivity Apps
生产力应用
swift
@MainActor
@Animatable
struct TimerView: View {
var remainingSeconds: Double
var body: some View {
let minutes = Int(remainingSeconds) / 60
let seconds = Int(remainingSeconds) % 60
Text(String(format: "%02d:%02d", minutes, seconds))
.font(.system(.largeTitle, design: .monospaced))
}
}Use case: Progress bars, countdown timers, percentage indicators, task completion metrics
swift
@MainActor
@Animatable
struct TimerView: View {
var remainingSeconds: Double
var body: some View {
let minutes = Int(remainingSeconds) / 60
let seconds = Int(remainingSeconds) % 60
Text(String(format: "%02d:%02d", minutes, seconds))
.font(.system(.largeTitle, design: .monospaced))
}
}应用场景:进度条、倒计时器、百分比指示器、任务完成指标
Complete Example
完整示例
swift
struct ContentView: View {
@State private var stockPrice: Double = 142.50
var body: some View {
VStack(spacing: 20) {
StockPriceView(price: stockPrice, changePercent: 0.025)
.animation(.spring(duration: 0.8), value: stockPrice)
Button("Simulate Price Change") {
stockPrice = Double.random(in: 130...160)
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
@MainActor
@Animatable
struct StockPriceView: View {
var price: Double
var changePercent: Double
var body: some View {
VStack(alignment: .trailing) {
Text("$\(price, format: .number.precision(.fractionLength(2)))")
.font(.title)
.fontWeight(.semibold)
Text("\(changePercent > 0 ? "+" : "")\(changePercent, format: .percent.precision(.fractionLength(2)))")
.font(.subheadline)
.foregroundColor(changePercent > 0 ? .green : .red)
}
}
}Result: Smooth, natural animation of stock price changes that feels professional and polished.
swift
struct ContentView: View {
@State private var stockPrice: Double = 142.50
var body: some View {
VStack(spacing: 20) {
StockPriceView(price: stockPrice, changePercent: 0.025)
.animation(.spring(duration: 0.8), value: stockPrice)
Button("Simulate Price Change") {
stockPrice = Double.random(in: 130...160)
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
@MainActor
@Animatable
struct StockPriceView: View {
var price: Double
var changePercent: Double
var body: some View {
VStack(alignment: .trailing) {
Text("$\(price, format: .number.precision(.fractionLength(2)))")
.font(.title)
.fontWeight(.semibold)
Text("\(changePercent > 0 ? "+" : "")\(changePercent, format: .percent.precision(.fractionLength(2)))")
.font(.subheadline)
.foregroundColor(changePercent > 0 ? .green : .red)
}
}
}结果:流畅、自然的股价变化动画,让UI看起来专业且精致。
Part 4: Animation Types
第四部分:动画类型
Timing Curve Animations
时间曲线动画
Timing curve animations use bezier curves to control the speed of animation over time.
时间曲线动画使用贝塞尔曲线来控制动画随时间变化的速度。
Built-in presets
内置预设
swift
.animation(.linear) // Constant speed
.animation(.easeIn) // Starts slow, ends fast
.animation(.easeOut) // Starts fast, ends slow
.animation(.easeInOut) // Slow start and end, fast middleswift
.animation(.linear) // 匀速
.animation(.easeIn) // 慢开始,快结束
.animation(.easeOut) // 快开始,慢结束
.animation(.easeInOut) // 慢开始和结束,中间快Custom timing curves
自定义时间曲线
swift
let customCurve = UnitCurve(
startControlPoint: CGPoint(x: 0.2, y: 0),
endControlPoint: CGPoint(x: 0.8, y: 1)
)
.animation(.timingCurve(customCurve, duration: 0.5))swift
let customCurve = UnitCurve(
startControlPoint: CGPoint(x: 0.2, y: 0),
endControlPoint: CGPoint(x: 0.8, y: 1)
)
.animation(.timingCurve(customCurve, duration: 0.5))Duration
时长
All timing curve animations accept an optional duration:
swift
.animation(.easeInOut(duration: 0.3))
.animation(.linear(duration: 1.0))Default: 0.35 seconds
所有时间曲线动画都支持可选的时长参数:
swift
.animation(.easeInOut(duration: 0.3))
.animation(.linear(duration: 1.0))默认值:0.35秒
Spring Animations
弹簧动画
Spring animations use physics simulation to create natural, organic motion.
弹簧动画使用物理模拟来创建自然、有机的运动效果。
Built-in presets
内置预设
swift
.animation(.smooth) // No bounce (default since iOS 17)
.animation(.snappy) // Small amount of bounce
.animation(.bouncy) // Larger amount of bounceswift
.animation(.smooth) // 无弹跳(iOS 17起默认)
.animation(.snappy) // 轻微弹跳
.animation(.bouncy) // 明显弹跳Custom springs
自定义弹簧
swift
.animation(.spring(duration: 0.6, bounce: 0.3))Parameters
- — Perceived animation duration
duration - — Amount of bounce (0 = no bounce, 1 = very bouncy)
bounce
Much more intuitive than traditional spring parameters (mass, stiffness, damping).
swift
.animation(.spring(duration: 0.6, bounce: 0.3))参数
- —— 感知到的动画时长
duration - —— 弹跳程度(0=无弹跳,1=强弹跳)
bounce
比传统弹簧参数更直观(质量、刚度、阻尼)。
Higher-Order Animations
高阶动画
Modify base animations to create complex effects.
修改基础动画以创建复杂效果。
Delay
延迟
swift
.animation(.spring.delay(0.5))Waits 0.5 seconds before starting the animation.
swift
.animation(.spring.delay(0.5))在开始动画前等待0.5秒。
Repeat
重复
swift
.animation(.easeInOut.repeatCount(3, autoreverses: true))
.animation(.linear.repeatForever(autoreverses: false))Repeats the animation multiple times or infinitely.
swift
.animation(.easeInOut.repeatCount(3, autoreverses: true))
.animation(.linear.repeatForever(autoreverses: false))多次重复动画或无限重复。
Speed
速度
swift
.animation(.spring.speed(2.0)) // 2x faster
.animation(.spring.speed(0.5)) // 2x slowerMultiplies the animation speed.
swift
.animation(.spring.speed(2.0)) // 2倍速
.animation(.spring.speed(0.5)) // 0.5倍速乘以动画速度。
Default Animation Changes (iOS 17+)
默认动画变化(iOS 17+)
Before iOS 17
swift
withAnimation {
// Used timing curve by default
}iOS 17+
swift
withAnimation {
// Uses .smooth spring by default
}Why the change: Spring animations feel more natural and preserve velocity when interrupted.
Recommendation: Embrace springs. They make your UI feel more responsive and polished.
iOS 17之前
swift
withAnimation {
// 默认使用时间曲线
}iOS 17+
swift
withAnimation {
// 默认使用.smooth弹簧动画
}变化原因:弹簧动画感觉更自然,在被中断时能保留速度。
建议:使用弹簧动画。它们让UI感觉更具响应性和精致感。
Part 5: Transaction System
第五部分:Transaction系统
withAnimation
withAnimation
The most common way to trigger an animation.
swift
Button("Scale Up") {
withAnimation(.spring) {
scale = 1.5
}
}How it works
- opens a transaction
withAnimation - Sets the animation in the transaction dictionary
- Executes the closure (state changes)
- Transaction propagates down the view hierarchy
- Animatable attributes check for animation and interpolate
触发动画最常用的方式。
swift
Button("Scale Up") {
withAnimation(.spring) {
scale = 1.5
}
}工作原理
- 开启一个事务
withAnimation - 在事务字典中设置动画
- 执行闭包(状态变化)
- 事务向下传播到视图层级
- 可动画属性检查动画并进行插值
Explicit animation
显式动画
swift
withAnimation(.spring(duration: 0.6, bounce: 0.4)) {
isExpanded.toggle()
}swift
withAnimation(.spring(duration: 0.6, bounce: 0.4)) {
isExpanded.toggle()
}No animation
无动画
swift
withAnimation(nil) {
// Changes happen immediately, no animation
resetState()
}swift
withAnimation(nil) {
// 变化立即发生,无动画
resetState()
}animation() View Modifier
animation() 视图修饰符
Apply animations to specific values within a view.
为视图内的特定值应用动画。
Basic usage
基本用法
swift
Circle()
.fill(isActive ? .blue : .gray)
.animation(.spring, value: isActive)How it works: Animation only applies when changes. Other state changes won't trigger this animation.
isActiveswift
Circle()
.fill(isActive ? .blue : .gray)
.animation(.spring, value: isActive)工作原理:仅当变化时才应用动画。其他状态变化不会触发该动画。
isActiveMultiple animations on same view
同一视图的多个动画
swift
Circle()
.scaleEffect(scale)
.animation(.bouncy, value: scale)
.opacity(opacity)
.animation(.easeInOut, value: opacity)Different animations for different properties.
swift
Circle()
.scaleEffect(scale)
.animation(.bouncy, value: scale)
.opacity(opacity)
.animation(.easeInOut, value: opacity)为不同属性设置不同的动画。
Scoped Animations (iOS 17+)
范围动画(iOS 17+)
Narrowly scope animations to specific animatable attributes.
将动画精确限定到特定的可动画属性。
Problem with old approach
旧方式的问题
swift
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)
}
}swift
struct AvatarView: View {
var selected: Bool
var body: some View {
Image("avatar")
.scaleEffect(selected ? 1.5 : 1.0)
.animation(.spring, value: selected)
// ⚠️ 如果选中时图片也发生变化,图片转场也会被意外添加动画
}
}Solution: Scoped animation
解决方案:范围动画
swift
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
}
}How it works
- Animation only applies to attributes in the closure
- Other attributes are unaffected
- Prevents accidental animations
swift
struct AvatarView: View {
var selected: Bool
var body: some View {
Image("avatar")
.animation(.spring, value: selected) {
$0.scaleEffect(selected ? 1.5 : 1.0)
}
// ✅ 仅scaleEffect会动画,图片转场不会
}
}工作原理
- 动画仅应用于闭包内的属性
- 其他属性不受影响
- 防止意外动画
Custom Transaction Keys
自定义Transaction键
Define your own transaction values to propagate custom context.
定义自己的事务值以传播自定义上下文。
Define a key
定义键
swift
struct AvatarTappedKey: TransactionKey {
static let defaultValue: Bool = false
}
extension Transaction {
var avatarTapped: Bool {
get { self[AvatarTappedKey.self] }
set { self[AvatarTappedKey.self] = newValue }
}
}swift
struct AvatarTappedKey: TransactionKey {
static let defaultValue: Bool = false
}
extension Transaction {
var avatarTapped: Bool {
get { self[AvatarTappedKey.self] }
set { self[AvatarTappedKey.self] = newValue }
}
}Set value in transaction
在事务中设置值
swift
var transaction = Transaction()
transaction.avatarTapped = true
withTransaction(transaction) {
isSelected.toggle()
}swift
var transaction = Transaction()
transaction.avatarTapped = true
withTransaction(transaction) {
isSelected.toggle()
}Read value in view
在视图中读取值
swift
.transaction { transaction in
if transaction.avatarTapped {
transaction.animation = .bouncy
} else {
transaction.animation = .smooth
}
}Use case: Apply different animations based on how the state change was triggered (tap vs programmatic).
swift
.transaction { transaction in
if transaction.avatarTapped {
transaction.animation = .bouncy
} else {
transaction.animation = .smooth
}
}应用场景:根据状态变化的触发方式(点击 vs 程序化)应用不同的动画。
Part 6: Advanced Topics
第六部分:高级主题
CustomAnimation Protocol
CustomAnimation协议
Implement your own animation algorithms.
swift
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?
}实现自己的动画算法。
swift
protocol CustomAnimation {
// 计算当前值
func animate<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: inout AnimationContext<V>
) -> V?
// 可选:该动画是否应与前一个合并?
func shouldMerge<V>(previous: Animation, value: V, time: TimeInterval, context: inout AnimationContext<V>) -> Bool
// 可选:当前速度
func velocity<V: VectorArithmetic>(
value: V,
time: TimeInterval,
context: AnimationContext<V>
) -> V?
}Example: Linear timing curve
示例:线性时间曲线
swift
struct LinearAnimation: CustomAnimation {
let duration: TimeInterval
func animate<V: VectorArithmetic>(
value: V, // Delta vector: target - current
time: TimeInterval, // Elapsed time since animation started
context: inout AnimationContext<V>
) -> V? {
// Animation is done when time exceeds duration
if time >= duration {
return nil
}
// Calculate linear progress (0.0 to 1.0)
let progress = time / duration
// Scale the delta vector by progress
// This returns how much to move FROM current position
// NOT the final target position
return value.scaled(by: progress)
}
}Critical understanding: The parameter is the delta vector (target - current), not the target value itself.
valueExample in practice:
- Current position:
10.0 - Target position:
100.0 - Delta vector passed to :
animate()(target - current)90.0 - At 50% progress: → returns
return value.scaled(by: 0.5)45.0 - SwiftUI adds this to current: (halfway to target) ✅
10.0 + 45.0 = 55.0
Common mistake:
swift
// ❌ WRONG: Treating value as the target
let progress = time / duration
return value.scaled(by: progress) // This assumes value is delta
// ❌ WRONG: Trying to interpolate manually
let target = value // No! value is already the delta
return current + (target - current) * progress // Incorrect
// ✅ CORRECT: Scale the delta
return value.scaled(by: progress) // SwiftUI handles the additionswift
struct LinearAnimation: CustomAnimation {
let duration: TimeInterval
func animate<V: VectorArithmetic>(
value: V, // 差值向量:目标值 - 当前值
time: TimeInterval, // 动画开始以来的经过时间
context: inout AnimationContext<V>
) -> V? {
// 当时间超过时长时,动画结束
if time >= duration {
return nil
}
// 计算线性进度(0.0到1.0)
let progress = time / duration
// 按进度缩放差值向量
// 返回的是从当前位置需要移动的量
// 不是最终的目标位置
return value.scaled(by: progress)
}
}关键理解:参数是差值向量(目标值 - 当前值),而不是目标值本身。
value实际示例:
- 当前位置:
10.0 - 目标位置:
100.0 - 传递给的差值向量:
animate()(目标值 - 当前值)90.0 - 50%进度时:→ 返回
return value.scaled(by: 0.5)45.0 - SwiftUI将其加到当前值上:(正好是中间值) ✅
10.0 + 45.0 = 55.0
常见错误:
swift
// ❌ 错误:将value视为目标值
let progress = time / duration
return value.scaled(by: progress) // 这假设value是差值
// ❌ 错误:尝试手动插值
let target = value // 不对!value已经是差值了
return current + (target - current) * progress // 错误
// ✅ 正确:缩放差值
return value.scaled(by: progress) // SwiftUI会处理加法Animation Merging Behavior
动画合并行为
What happens when a new animation starts before the previous one finishes?
当新动画在前一个动画完成前启动时会发生什么?
Timing curve animations (default: don't merge)
时间曲线动画(默认:不合并)
swift
func shouldMerge(...) -> Bool {
return false // Default implementation
}Behavior: Both animations run together, results are combined additively.
Example
- First tap: animate 1.0 → 1.5 (running)
- Second tap (before finish): animate 1.5 → 1.0
- Result: Both animations run, values combine
swift
func shouldMerge(...) -> Bool {
return false // 默认实现
}行为:两个动画同时运行,结果叠加。
示例
- 第一次点击:动画从1.0→1.5(运行中)
- 第二次点击(未完成时):动画从1.5→1.0
- 结果:两个动画同时运行,值叠加
Spring animations (merge and retarget)
弹簧动画(合并并重定向)
swift
func shouldMerge(...) -> Bool {
return true // Springs override this
}Behavior: New animation incorporates state of previous animation, preserving velocity.
Example
- First tap: animate 1.0 → 1.5 with velocity V
- Second tap (before finish): retarget to 1.0, preserving current velocity V
- Result: Smooth transition, no sudden velocity change
Why springs feel more natural: They preserve momentum when interrupted.
swift
func shouldMerge(...) -> Bool {
return true // 弹簧动画覆盖此实现
}行为:新动画会结合前一个动画的状态,保留速度。
示例
- 第一次点击:以速度V从1.0→1.5动画
- 第二次点击(未完成时):重定向到1.0,保留当前速度V
- 结果:平滑过渡,无突然的速度变化
为何弹簧动画更自然:它们在被中断时能保留动量。
Part 7: Zoom Transitions (iOS 18+)
第七部分:缩放过渡(iOS 18+)
Overview
概述
iOS 18 introduces the zoom transition, where a tapped cell morphs into the incoming view. This transition is continuously interactive—users can grab and drag the view during or after the transition begins.
Key benefit In parts of your app where you transition from a large cell, zoom transitions increase visual continuity by keeping the same UI elements on screen across the transition.
iOS 18引入了缩放过渡,点击的单元格会变形为进入的视图。这种过渡是连续交互的——用户可以在过渡开始期间或之后抓取并拖动视图。
主要优势 在应用中从大型单元格过渡的场景中,缩放过渡通过在过渡过程中保持相同的UI元素在屏幕上,提升了视觉连续性。
SwiftUI Implementation
SwiftUI实现
Two steps to adopt zoom transitions:
采用缩放过渡分两步:
Step 1: Declare the transition style on the destination
步骤1:在目标视图上声明过渡样式
swift
NavigationLink {
BraceletEditor(bracelet)
.navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
} label: {
BraceletPreview(bracelet)
}swift
NavigationLink {
BraceletEditor(bracelet)
.navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
} label: {
BraceletPreview(bracelet)
}Step 2: Mark the source view
步骤2:标记源视图
swift
BraceletPreview(bracelet)
.matchedTransitionSource(id: bracelet.id, in: namespace)swift
BraceletPreview(bracelet)
.matchedTransitionSource(id: bracelet.id, in: namespace)Complete example
完整示例
swift
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)
}
}
}
}
}
}swift
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 Implementation
UIKit实现
swift
func showEditor(for bracelet: Bracelet) {
let braceletEditor = BraceletEditorViewController(bracelet: bracelet)
// Step 1: Specify zoom transition on the pushed view controller
braceletEditor.preferredTransition = .zoom { context in
// Step 2: Return the source view
let editor = context.zoomedViewController as! BraceletEditorViewController
return self.cell(for: editor.bracelet)
}
navigationController?.pushViewController(braceletEditor, animated: true)
}Critical detail The closure is called on both zoom in and zoom out. Capture a stable identifier (like the model object), not a view directly—the source view may get reused in a collection view.
swift
func showEditor(for bracelet: Bracelet) {
let braceletEditor = BraceletEditorViewController(bracelet: bracelet)
// 步骤1:在推入的视图控制器上指定缩放过渡
braceletEditor.preferredTransition = .zoom { context in
// 步骤2:返回源视图
let editor = context.zoomedViewController as! BraceletEditorViewController
return self.cell(for: editor.bracelet)
}
navigationController?.pushViewController(braceletEditor, animated: true)
}关键细节 闭包会在放大和缩小时都被调用。捕获稳定的标识符(如模型对象),而不是直接捕获视图——源视图可能会在集合视图中被重用。
Handling content changes during presentation
处理展示期间的内容变化
If the editor's content can change (e.g., swiping between items), use the context to retrieve the current item:
swift
braceletEditor.preferredTransition = .zoom { context in
let editor = context.zoomedViewController as! BraceletEditorViewController
// Use current bracelet, not the one captured at push time
return self.cell(for: editor.bracelet)
}如果编辑器的内容可能变化(例如,在项目间滑动),使用上下文获取当前项目:
swift
braceletEditor.preferredTransition = .zoom { context in
let editor = context.zoomedViewController as! BraceletEditorViewController
// 使用当前的bracelet,而不是推入时捕获的那个
return self.cell(for: editor.bracelet)
}Presentations (Sheets and Full Screen Covers)
展示方式(Sheets和全屏覆盖)
Zoom transitions work with and in both SwiftUI and UIKit:
fullScreenCoversheetswift
.fullScreenCover(item: $selectedBracelet) { bracelet in
BraceletEditor(bracelet: bracelet)
.navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
}缩放过渡在SwiftUI和UIKit中都可用于和:
fullScreenCoversheetswift
.fullScreenCover(item: $selectedBracelet) { bracelet in
BraceletEditor(bracelet: bracelet)
.navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
}Styling the Source View
样式化源视图
Use the configuration closure to style the source during transition:
swift
.matchedTransitionSource(id: bracelet.id, in: namespace) { source in
source
.cornerRadius(8.0)
.shadow(radius: 4)
}Modifiers applied here are smoothly interpolated during the zoom transition.
使用配置闭包在过渡期间样式化源视图:
swift
.matchedTransitionSource(id: bracelet.id, in: namespace) { source in
source
.cornerRadius(8.0)
.shadow(radius: 4)
}此处应用的修饰符会在缩放过渡期间平滑插值。
View Controller Lifecycle with Fluid Transitions
流畅过渡下的视图控制器生命周期
Key insight Push transitions cannot be cancelled. When interrupted, they convert to pop transitions.
关键要点 推入过渡无法取消。当被中断时,它们会转换为弹出过渡。
Normal push (no interaction)
正常推入(无交互)
Disappeared → [viewWillAppear] → Appearing → [viewIsAppearing] → [viewDidAppear] → AppearedDisappeared → [viewWillAppear] → Appearing → [viewIsAppearing] → [viewDidAppear] → AppearedInterrupted push (user starts pop during push)
被中断的推入(用户在推入期间开始弹出)
Appearing → Appeared → Disappearing → ...The push completes immediately, then the pop begins. The view controller always reaches the Appeared state—callbacks complete their full cycle for consistency.
Appearing → Appeared → Disappearing → ...推入会立即完成,然后开始弹出。视图控制器总会到达Appeared状态——回调会完整执行以保证一致性。
UIKit Best Practices for Fluid Transitions
流畅过渡的UIKit最佳实践
swift
// ❌ DON'T: Block actions during transitions
func handleTap() {
guard !isTransitioning else { return } // Don't do this
pushViewController(...)
}
// ✅ DO: Always allow the action
func handleTap() {
pushViewController(...) // System handles overlapping transitions
}Guidelines
- Be ready for a new transition to start at any time
- Keep temporary transition state to a minimum
- Reset transition state in or
viewDidAppearviewDidDisappear - Consider adopting SwiftUI for complex transition logic
swift
// ❌ 不要:在过渡期间阻止操作
func handleTap() {
guard !isTransitioning else { return } // 不要这样做
pushViewController(...)
}
// ✅ 要:始终允许操作
func handleTap() {
pushViewController(...) // 系统会处理重叠过渡
}指导原则
- 随时准备好新过渡的启动
- 尽量减少临时过渡状态
- 在或
viewDidAppear中重置过渡状态viewDidDisappear - 考虑采用SwiftUI处理复杂的过渡逻辑
Part 8: UIKit/AppKit Animation Bridging (iOS 18+)
第八部分:UIKit/AppKit动画桥接(iOS 18+)
Overview
概述
iOS 18 enables using SwiftUI types to animate UIKit and AppKit views. This provides access to the full suite of SwiftUI animations, including custom animations.
AnimationiOS 18允许使用SwiftUI 类型为UIKit和AppKit视图添加动画。这让你可以使用全套SwiftUI动画,包括自定义动画。
AnimationAPI Signature
API签名
swift
@MainActor static func animate(
_ animation: Animation,
changes: () -> Void,
completion: (() -> Void)? = nil
)swift
@MainActor static func animate(
_ animation: Animation,
changes: () -> Void,
completion: (() -> Void)? = nil
)Basic Usage
基本用法
swift
// Old way: Describe spring in parameters
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
}swift
// 旧方式:用参数描述弹簧
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5) {
bead.center = endOfBracelet
}
// 新方式:使用SwiftUI Animation类型
UIView.animate(.spring(duration: 0.5)) {
bead.center = endOfBracelet
}Available Animation Types
支持的动画类型
All SwiftUI animations work with UIKit views:
swift
// Timing curves
UIView.animate(.linear(duration: 0.3)) { ... }
UIView.animate(.easeIn(duration: 0.3)) { ... }
UIView.animate(.easeOut(duration: 0.3)) { ... }
UIView.animate(.easeInOut(duration: 0.3)) { ... }
// Springs
UIView.animate(.spring) { ... }
UIView.animate(.spring(duration: 0.6, bounce: 0.3)) { ... }
UIView.animate(.smooth) { ... }
UIView.animate(.snappy) { ... }
UIView.animate(.bouncy) { ... }
// Repeating
UIView.animate(.linear(duration: 1.3).repeatForever()) { ... }
// Custom animations
UIView.animate(myCustomAnimation) { ... }所有SwiftUI动画都可用于UIKit视图:
swift
// 时间曲线
UIView.animate(.linear(duration: 0.3)) { ... }
UIView.animate(.easeIn(duration: 0.3)) { ... }
UIView.animate(.easeOut(duration: 0.3)) { ... }
UIView.animate(.easeInOut(duration: 0.3)) { ... }
// 弹簧
UIView.animate(.spring) { ... }
UIView.animate(.spring(duration: 0.6, bounce: 0.3)) { ... }
UIView.animate(.smooth) { ... }
UIView.animate(.snappy) { ... }
UIView.animate(.bouncy) { ... }
// 重复
UIView.animate(.linear(duration: 1.3).repeatForever()) { ... }
// 自定义动画
UIView.animate(myCustomAnimation) { ... }Implementation Detail: No CAAnimation
实现细节:无CAAnimation
Important architectural difference:
| Old UIKit API | New SwiftUI Animation API |
|---|---|
Generates a | No |
| Animation added to layer | Animates presentation values directly |
Animation in layer's | Presentation values in presentation layer |
Both approaches reflect values in the presentation layer, but the mechanism differs.
重要架构差异:
| 旧UIKit API | 新SwiftUI Animation API |
|---|---|
生成 | 不生成 |
| 动画添加到图层 | 直接为展示值添加动画 |
动画在图层的 | 展示值在展示图层中 |
两种方式都会在展示图层中反映值,但机制不同。
Complete Example
完整示例
swift
class BeadViewController: UIViewController {
private var animatingView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
animatingView = UIImageView(image: UIImage(systemName: "circle.fill"))
animatingView.tintColor = .systemPink
animatingView.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
view.addSubview(animatingView)
animatingView.center = view.center
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startAnimating()
}
private func startAnimating() {
let animation = Animation
.linear(duration: 1.3)
.repeatForever()
UIView.animate(animation) { [weak self] in
self?.animatingView.transform = CGAffineTransform(scaleX: 2, y: 2)
}
}
}swift
class BeadViewController: UIViewController {
private var animatingView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
animatingView = UIImageView(image: UIImage(systemName: "circle.fill"))
animatingView.tintColor = .systemPink
animatingView.frame = CGRect(x: 0, y: 0, width: 80, height: 80)
view.addSubview(animatingView)
animatingView.center = view.center
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startAnimating()
}
private func startAnimating() {
let animation = Animation
.linear(duration: 1.3)
.repeatForever()
UIView.animate(animation) { [weak self] in
self?.animatingView.transform = CGAffineTransform(scaleX: 2, y: 2)
}
}
}Part 9: UIViewRepresentable Animation Bridging (iOS 18+)
第九部分:UIViewRepresentable动画桥接(iOS 18+)
The Problem
问题
When wrapping UIKit views in SwiftUI, animations don't automatically bridge:
swift
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在SwiftUI中包装UIKit视图时,动画不会自动桥接:
swift
struct BeadBoxWrapper: UIViewRepresentable {
@Binding var isOpen: Bool
func updateUIView(_ box: BeadBox, context: Context) {
// ❌ 绑定的动画不会影响UIKit
box.lid.center.y = isOpen ? -100 : 100
}
}
// 使用方式
BeadBoxWrapper(isOpen: $isOpen)
.animation(.spring, value: isOpen) // 对UIKit视图无效果The Solution: context.animate()
解决方案:context.animate()
Use to bridge SwiftUI animations:
context.animate()swift
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动画:
context.animate()swift
struct BeadBoxWrapper: UIViewRepresentable {
@Binding var isOpen: Bool
func makeUIView(context: Context) -> BeadBox {
BeadBox()
}
func updateUIView(_ box: BeadBox, context: Context) {
// ✅ 将Transaction中的动画桥接到UIKit
context.animate {
box.lid.center.y = isOpen ? -100 : 100
}
}
}How It Works
工作原理
- SwiftUI stores animation info in the current
Transaction - reads the Transaction's animation
context.animate() - Applies that animation to UIView changes in the closure
- If no animation in Transaction, changes happen immediately (no animation)
- SwiftUI将动画信息存储在当前的中
Transaction - 读取Transaction中的动画
context.animate() - 将该动画应用到闭包中的UIView变化
- 如果Transaction中没有动画,变化会立即发生(无动画)
Key Behavior
关键行为
swift
context.animate {
// Changes here
} completion: {
// Called when animation completes
// If not animated, called immediately inline
}Works whether animated or not — safe to always use this pattern.
swift
context.animate {
// 此处的变化
} completion: {
// 动画完成时调用
// 如果没有动画,会立即内联调用
}无论是否有动画都能工作——始终使用这种模式是安全的。
Perfect Synchronization
完美同步
A single animation running across SwiftUI Views and UIViews runs perfectly in sync. This enables seamless mixed hierarchies.
在SwiftUI视图和UIView之间运行的单个动画会完美同步。这支持无缝的混合层级。
Part 10: Gesture-Driven Animations (iOS 18+)
第十部分:手势驱动动画(iOS 18+)
The Problem with Manual Velocity
手动速度计算的问题
Traditional UIKit gesture animations require manual velocity calculation:
swift
// Old way: Manual velocity computation
func handlePan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .changed:
bead.center = gesture.location(in: view)
case .ended:
let velocity = gesture.velocity(in: view)
let distance = endOfBracelet.distance(to: bead.center)
// 😫 Convert to unit velocity manually
let unitVelocity = CGVector(
dx: velocity.x / distance,
dy: velocity.y / distance
)
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: unitVelocity.length) {
bead.center = endOfBracelet
}
}
}传统的UIKit手势动画需要手动计算速度:
swift
// 旧方式:手动计算速度
func handlePan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .changed:
bead.center = gesture.location(in: view)
case .ended:
let velocity = gesture.velocity(in: view)
let distance = endOfBracelet.distance(to: bead.center)
// 😫手动转换为单位速度
let unitVelocity = CGVector(
dx: velocity.x / distance,
dy: velocity.y / distance
)
UIView.animate(withDuration: 0.5,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: unitVelocity.length) {
bead.center = endOfBracelet
}
}
}SwiftUI Solution: Automatic Velocity Preservation
SwiftUI解决方案:自动速度保留
SwiftUI animations automatically preserve velocity through animation merging:
swift
// New way: Automatic velocity preservation
func handlePan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .changed:
// Interactive spring during drag
UIView.animate(.interactiveSpring) {
bead.center = gesture.location(in: view)
}
case .ended:
// Final spring uses velocity from interactiveSprings
UIView.animate(.spring) {
bead.center = endOfBracelet
}
}
}SwiftUI动画会通过动画合并自动保留速度:
swift
// 新方式:自动保留速度
func handlePan(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .changed:
// 拖动时的交互式弹簧
UIView.animate(.interactiveSpring) {
bead.center = gesture.location(in: view)
}
case .ended:
// 最终的弹簧动画会继承interactiveSprings的速度
UIView.animate(.spring) {
bead.center = endOfBracelet
}
}
}How Velocity Preservation Works
速度保留的工作原理
[Drag starts]
↓
[.changed] → interactiveSpring animation (retargets previous)
↓
[.changed] → interactiveSpring animation (retargets previous)
↓
[.changed] → interactiveSpring animation (retargets previous)
↓
[.ended] → .spring animation inherits velocity from interactiveSprings
↓
[Smooth deceleration to final position]No velocity calculation needed — SwiftUI handles it automatically.
[拖动开始]
↓
[.changed] → interactiveSpring动画(重定向前一个)
↓
[.changed] → interactiveSpring动画(重定向前一个)
↓
[.changed] → interactiveSpring动画(重定向前一个)
↓
[.ended] → .spring动画继承interactiveSprings的速度
↓
[平滑减速到最终位置]无需手动计算速度——SwiftUI会自动处理。
SwiftUI Equivalent
SwiftUI等效实现
swift
struct DraggableBead: View {
@State private var position: CGPoint = .zero
@State private var isDragging = false
var body: some View {
Circle()
.position(position)
.gesture(
DragGesture()
.onChanged { value in
withAnimation(.interactiveSpring) {
position = value.location
}
}
.onEnded { value in
withAnimation(.spring) {
position = targetPosition
}
}
)
}
}swift
struct DraggableBead: View {
@State private var position: CGPoint = .zero
@State private var isDragging = false
var body: some View {
Circle()
.position(position)
.gesture(
DragGesture()
.onChanged { value in
withAnimation(.interactiveSpring) {
position = value.location
}
}
.onEnded { value in
withAnimation(.spring) {
position = targetPosition
}
}
)
}
}Why This Matters
为何这很重要
Continuous velocity creates natural, physical-feeling interactions:
- No jarring velocity discontinuities
- Momentum carries through gesture end
- Spring animations feel connected to user input
连续速度创造了自然、符合物理规律的交互:
- 没有突兀的速度不连续
- 动量会延续到手势结束
- 弹簧动画感觉与用户输入相关联
Off-Main-Thread Performance
主线程外的性能
Built-in animatable attributes run efficiently:
swift
.scaleEffect(scale)
.opacity(opacity)
.rotationEffect(angle)Benefits
- Runs off the main thread
- Doesn't call your view's
body - Minimal performance impact
Custom Animatable conformance runs on main thread:
swift
@MainActor
@Animatable
struct MyView: View {
var value: Double
var animatableData: Double {
get { value }
set { value = newValue }
}
var body: some View {
// Called every frame! (main thread)
}
}Performance tip: Profile with Instruments if you have many custom animatable views.
内置可动画属性的运行效率很高:
swift
.scaleEffect(scale)
.opacity(opacity)
.rotationEffect(angle)优势
- 在主线程外运行
- 不会调用你的视图
body - 性能影响极小
自定义Animatable协议遵循在主线程运行:
swift
@MainActor
@Animatable
struct MyView: View {
var value: Double
var animatableData: Double {
get { value }
set { value = newValue }
}
var body: some View {
// 每一帧都会被调用!(主线程)
}
}性能提示:如果你有很多自定义可动画视图,使用Instruments进行分析。
Delta Vector Logic
差值向量逻辑
SwiftUI animates the difference between values, not the values themselves.
SwiftUI动画的是值之间的差值,而不是值本身。
Example: Scale effect
示例:缩放效果
swift
// User taps, scale changes from 1.0 to 1.5
.scaleEffect(isSelected ? 1.5 : 1.0)What SwiftUI actually animates
- Delta vector: 1.5 - 1.0 = 0.5
- Animation interpolates: 0.0 → 0.1 → 0.2 → 0.3 → 0.4 → 0.5
- Final value: 1.0 + interpolated delta
Why this matters
- Makes animation merging easier
- Allows additive combination of animations
- Simplifies CustomAnimation implementations
swift
// 用户点击,缩放从1.0变为1.5
.scaleEffect(isSelected ? 1.5 : 1.0)SwiftUI实际动画的内容
- 差值向量:1.5 - 1.0 = 0.5
- 动画插值:0.0 → 0.1 → 0.2 → 0.3 → 0.4 → 0.5
- 最终值:1.0 + 插值后的差值
为何这很重要
- 让动画合并更容易
- 允许动画的叠加组合
- 简化CustomAnimation实现
Troubleshooting
故障排除
Property Not Animating
属性无法动画
Symptom: Property changes but doesn't animate.
症状:属性变化但没有动画效果。
Cause 1: Type doesn't conform to VectorArithmetic
原因1:类型不遵循VectorArithmetic
swift
@State private var count: Int = 0 // ❌ Int doesn't animate
// Solution
@State private var count: Double = 0 // ✅ Double animates
Text("\(Int(count))") // Display as Intswift
@State private var count: Int = 0 // ❌ Int无法动画
// 解决方案
@State private var count: Double = 0 // ✅ Double可以动画
Text("\(Int(count))") // 显示为IntCause 2: Missing animation modifier
原因2:缺少动画修饰符
swift
// ❌ No animation specified
Text("\(value)")
// ✅ Add animation
Text("\(value)")
.animation(.spring, value: value)swift
// ❌ 未指定动画
Text("\(value)")
// ✅ 添加动画
Text("\(value)")
.animation(.spring, value: value)Cause 3: Wrong value in animation modifier
原因3:动画修饰符中的值错误
swift
struct ProgressView: View {
@State private var progress: Double = 0
@State private var title: String = "Loading"
var body: some View {
VStack {
Text(title)
ProgressBar(value: progress)
}
.animation(.spring, value: title) // ❌ Animates when title changes, not progress
}
}
// Solution
.animation(.spring, value: progress) // ✅swift
struct ProgressView: View {
@State private var progress: Double = 0
@State private var title: String = "Loading"
var body: some View {
VStack {
Text(title)
ProgressBar(value: progress)
}
.animation(.spring, value: title) // ❌ 仅在title变化时动画,不是progress
}
}
// 解决方案
.animation(.spring, value: progress) // ✅Cause 4: View doesn't conform to Animatable
原因4:视图不遵循Animatable
If you have a custom view with animatable properties:
swift
// ❌ Missing Animatable conformance
struct MyView: View {
var value: Double
var body: some View { ... }
}
// ✅ Add @Animatable macro (iOS 26+)
@MainActor
@Animatable
struct MyView: View {
var value: Double
var body: some View { ... }
}
// ✅ OR manual conformance (iOS 13+)
struct MyView: View, Animatable {
var value: Double
var animatableData: Double {
get { value }
set { value = newValue }
}
var body: some View { ... }
}如果你有带可动画属性的自定义视图:
swift
// ❌ 缺少Animatable协议遵循
struct MyView: View {
var value: Double
var body: some View { ... }
}
// ✅ 添加@Animatable宏(iOS 26+)
@MainActor
@Animatable
struct MyView: View {
var value: Double
var body: some View { ... }
}
// ✅ 或者手动遵循(iOS 13+)
struct MyView: View, Animatable {
var value: Double
var animatableData: Double {
get { value }
set { value = newValue }
}
var body: some View { ... }
}Animation Stuttering
动画卡顿
Symptom: Animation is choppy or drops frames.
症状:动画不流畅或丢帧。
Cause 1: Expensive body computation
原因1:body计算昂贵
swift
@MainActor
@Animatable
struct ExpensiveView: View {
var value: Double
var animatableData: Double {
get { value }
set { value = newValue }
}
var body: some View {
// ❌ Called every frame!
let heavyComputation = performExpensiveWork(value)
return Text("\(heavyComputation)")
}
}Solution: Use built-in effects instead
swift
struct OptimizedView: View {
@State private var value: Double = 0
var body: some View {
Text("\(computeOnce(value))")
.opacity(value) // ✅ Built-in effect, off-main-thread
}
}swift
@MainActor
@Animatable
struct ExpensiveView: View {
var value: Double
var animatableData: Double {
get { value }
set { value = newValue }
}
var body: some View {
// ❌ 每一帧都会被调用!
let heavyComputation = performExpensiveWork(value)
return Text("\(heavyComputation)")
}
}解决方案:改用内置效果
swift
struct OptimizedView: View {
@State private var value: Double = 0
var body: some View {
Text("\(computeOnce(value))")
.opacity(value) // ✅ 内置效果,主线程外运行
}
}Cause 2: Too many simultaneous animations
原因2:同时运行的动画过多
Profile with Instruments to identify bottlenecks.
使用Instruments分析以确定瓶颈。
Unexpected Animation Merging
意外的动画合并
Symptom: Animation behavior changes when interrupted.
Cause: Spring animations merge by default, preserving velocity from the previous animation.
Solution: Use a timing curve animation if you don't want merging behavior:
swift
// ❌ Spring merges with previous animation
withAnimation(.spring) {
scale = 1.0
}
// ✅ Timing curve starts fresh (additive, no merge)
withAnimation(.easeInOut(duration: 0.5)) {
scale = 1.0
}See Animation Merging Behavior section above for detailed explanation of merge vs additive animations.
症状:动画在被中断时行为变化。
原因:弹簧动画默认会合并,保留前一个动画的速度。
解决方案:如果你不想要合并行为,使用时间曲线动画:
swift
// ❌ 弹簧动画会与前一个动画合并
withAnimation(.spring) {
scale = 1.0
}
// ✅ 时间曲线动画会重新开始(叠加,不合并)
withAnimation(.easeInOut(duration: 0.5)) {
scale = 1.0
}请参阅上面的动画合并行为部分,了解合并动画与叠加动画的详细说明。
Resources
资源
WWDC: 2023-10156, 2023-10157, 2023-10158, 2024-10145, 2025-256
Docs: /swiftui/animatable, /swiftui/animation, /swiftui/vectorarithmetic, /swiftui/transaction, /swiftui/view/navigationtransition(:), /swiftui/view/matchedtransitionsource(id:in:configuration:), /uikit/uiview/animate(:changes:completion:)
Skills: axiom-swiftui-26-ref, axiom-swiftui-nav-ref, axiom-swiftui-performance, axiom-swiftui-debugging
Last Updated Based on WWDC 2023/10156-10158, WWDC 2024/10145, WWDC 2025/256
Version iOS 13+ (Animatable), iOS 17+ (scoped animations), iOS 18+ (zoom transitions, UIKit bridging), iOS 26+ (@Animatable)
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
最后更新 基于WWDC 2023/10156-10158, WWDC 2024/10145, WWDC 2025/256
版本 iOS 13+ (Animatable), iOS 17+ (范围动画), iOS 18+ (缩放过渡、UIKit桥接), iOS 26+ (@Animatable)