axiom-swiftui-animation-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI 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.0
How 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
VectorArithmetic
, which provides:
swift
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
    ,
    Float
    ,
    Angle
  • 2-dimensional:
    CGPoint
    ,
    CGSize
  • 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要求可动画的数据遵循
VectorArithmetic
协议,该协议提供以下能力:
swift
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
    ,
    Float
    ,
    Angle
  • 二维类型
    CGPoint
    ,
    CGSize
  • 四维类型
    CGRect
关键要点 向量运算抽象了可动画数据的维度。SwiftUI可以通过单一的泛型实现为所有这些类型添加动画。

Why Int Can't Be Animated

为何Int无法动画

Int
does not conform to VectorArithmetic because:
  1. No fractional intermediate values — There is no "3.5" between 3 and 4
  2. Not continuous — Integers are discrete values
  3. Scaling doesn't make sense — What does 5 × 0.5 mean for an integer?
Int
不遵循VectorArithmetic协议,原因如下:
  1. 没有分数中间值——3和4之间不存在“3.5”
  2. 非连续——整数是离散值
  3. 缩放无意义——整数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
0.0 → ... → 100.0
, and you display the rounded integer at each frame.
swift
struct AnimatedCounterView: View {
    @State private var count: Float = 0

    var body: some View {
        Text("\(Int(count))")
            .animation(.spring, value: count)
    }
}
结果:SwiftUI会对
0.0 → ... → 100.0
进行插值,你可以在每一帧显示取整后的整数。

Model 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
selected
becomes
true
:
  • Model value: Immediately becomes
    1.5
  • Presentation value: Interpolates
    1.0 → 1.1 → 1.2 → 1.3 → 1.4 → 1.5
    over time

  • 当前正在渲染的插值后的数值
  • 动画过程中逐帧更新
  • 用户实际看到的值
示例
swift
.scaleEffect(selected ? 1.5 : 1.0)
selected
变为
true
时:
  • 模型值:立即变为
    1.5
  • 展示值:在一段时间内从
    1.0 → 1.1 → 1.2 → 1.3 → 1.4 → 1.5
    进行插值

Part 2: Animatable Protocol

第二部分:Animatable协议

Overview

概述

The
Animatable
protocol allows views to animate their properties by defining which data should be interpolated.
swift
protocol Animatable {
    associatedtype AnimatableData: VectorArithmetic

    var animatableData: AnimatableData { get set }
}
SwiftUI builds an animatable attribute for any view conforming to this protocol.
Animatable
协议允许视图通过定义哪些数据需要被插值来为自身属性添加动画。
swift
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

视觉效果

  • .scaleEffect()
    — Animates scale transform
  • .rotationEffect()
    — Animates rotation
  • .offset()
    — Animates position offset
  • .opacity()
    — Animates transparency
  • .blur()
    — Animates blur radius
  • .shadow()
    — Animates shadow properties
  • .scaleEffect()
    —— 为缩放变换添加动画
  • .rotationEffect()
    —— 为旋转变换添加动画
  • .offset()
    —— 为位置偏移添加动画
  • .opacity()
    —— 为透明度添加动画
  • .blur()
    —— 为模糊半径添加动画
  • .shadow()
    —— 为阴影属性添加动画

All Shape types

所有Shape类型

  • Circle
    ,
    Rectangle
    ,
    RoundedRectangle
  • Capsule
    ,
    Ellipse
    ,
    Path
  • Custom
    Shape
    implementations
  • Circle
    ,
    Rectangle
    ,
    RoundedRectangle
  • Capsule
    ,
    Ellipse
    ,
    Path
  • 自定义
    Shape
    实现

AnimatablePair for Multi-Dimensional Data

用于多维数据的AnimatablePair

When animating multiple properties, use
AnimatablePair
to combine vectors.
当为多个属性添加动画时,使用
AnimatablePair
来组合向量。

Example: 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
  • CGSize
    is 2-dimensional (width, height)
  • UnitPoint
    is 2-dimensional (x, y)
  • AnimatablePair
    fuses them into a 4-dimensional vector
  • 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
    是二维的(宽度、高度)
  • UnitPoint
    是二维的(x、y)
  • 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
  1. number
    changes from 0 to 100
  2. SwiftUI calls
    body
    for every frame of the animation
  3. Each frame gets a new
    number
    value: 0 → 5 → 15 → 30 → 55 → 80 → 100
  4. 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)
工作原理
  1. number
    从0变为100
  2. SwiftUI会在动画的每一帧调用
    body
  3. 每一帧都会得到一个新的
    number
    值:0 → 5 → 15 → 30 → 55 → 80 → 100
  4. 文本会更新以显示插值后的整数

Performance Warning

性能警告

Custom Animatable conformance can be expensive.
When you conform a view to Animatable:
  • SwiftUI calls your view's
    body
    for every frame of the animation
  • Layout is rerun every frame
  • This happens on the main thread
Built-in animatable effects (like
.scaleEffect()
,
.opacity()
) are much more efficient:
  • 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
offsetAngle
requires recalculating positions every frame. No built-in modifier can do this.

swift
// 这种实现虽然性能消耗大,但却是为环形路径添加动画的必要方式
@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)
        }
    }
}
必要性:为
offsetAngle
添加动画需要逐帧重新计算位置。没有内置修饰符可以实现这一点。

Part 3: @Animatable Macro (iOS 26+)

第三部分:@Animatable宏(iOS 26+)

Overview

概述

The
@Animatable
macro eliminates the boilerplate of manually conforming to the Animatable protocol.
Before iOS 26, you had to:
  1. Manually conform to
    Animatable
  2. Write
    animatableData
    getter and setter
  3. Use
    AnimatablePair
    for multiple properties
  4. Exclude non-animatable properties manually
iOS 26+, you just add
@Animatable
:
swift
@MainActor
@Animatable
struct MyView: View {
    var scale: CGFloat
    var opacity: Double

    var body: some View {
        // ...
    }
}
The macro automatically:
  • Generates
    Animatable
    conformance
  • Inspects all stored properties
  • Creates
    animatableData
    from VectorArithmetic-conforming properties
  • Handles multi-dimensional data with
    AnimatablePair
@Animatable
宏消除了手动遵循Animatable协议的样板代码。
iOS 26之前,你需要:
  1. 手动遵循
    Animatable
    协议
  2. 编写
    animatableData
    的getter和setter
  3. 对多个属性使用
    AnimatablePair
  4. 手动排除不可动画的属性
iOS 26+,你只需添加
@Animatable
swift
@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
@AnimatableIgnored
to exclude properties from animation.
使用
@AnimatableIgnored
来排除不需要动画的属性。

When 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 middle
swift
.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 bounce
swift
.animation(.smooth)     // 无弹跳(iOS 17起默认)
.animation(.snappy)     // 轻微弹跳
.animation(.bouncy)     // 明显弹跳

Custom springs

自定义弹簧

swift
.animation(.spring(duration: 0.6, bounce: 0.3))
Parameters
  • duration
    — Perceived animation duration
  • bounce
    — Amount of bounce (0 = no bounce, 1 = very bouncy)
Much more intuitive than traditional spring parameters (mass, stiffness, damping).
swift
.animation(.spring(duration: 0.6, bounce: 0.3))
参数
  • duration
    —— 感知到的动画时长
  • bounce
    —— 弹跳程度(0=无弹跳,1=强弹跳)
比传统弹簧参数更直观(质量、刚度、阻尼)。

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 slower
Multiplies 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
  1. withAnimation
    opens a transaction
  2. Sets the animation in the transaction dictionary
  3. Executes the closure (state changes)
  4. Transaction propagates down the view hierarchy
  5. Animatable attributes check for animation and interpolate
触发动画最常用的方式。
swift
Button("Scale Up") {
    withAnimation(.spring) {
        scale = 1.5
    }
}
工作原理
  1. withAnimation
    开启一个事务
  2. 在事务字典中设置动画
  3. 执行闭包(状态变化)
  4. 事务向下传播到视图层级
  5. 可动画属性检查动画并进行插值

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
isActive
changes. Other state changes won't trigger this animation.
swift
Circle()
    .fill(isActive ? .blue : .gray)
    .animation(.spring, value: isActive)
工作原理:仅当
isActive
变化时才应用动画。其他状态变化不会触发该动画。

Multiple 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
value
parameter is the delta vector (target - current), not the target value itself.
Example in practice:
  • Current position:
    10.0
  • Target position:
    100.0
  • Delta vector passed to
    animate()
    :
    90.0
    (target - current)
  • At 50% progress:
    return value.scaled(by: 0.5)
    → returns
    45.0
  • SwiftUI adds this to current:
    10.0 + 45.0 = 55.0
    (halfway to target) ✅
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 addition
swift
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
fullScreenCover
and
sheet
in both SwiftUI and UIKit:
swift
.fullScreenCover(item: $selectedBracelet) { bracelet in
    BraceletEditor(bracelet: bracelet)
        .navigationTransition(.zoom(sourceID: bracelet.id, in: namespace))
}
缩放过渡在SwiftUI和UIKit中都可用于
fullScreenCover
sheet
swift
.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] → Appeared
Disappeared → [viewWillAppear] → Appearing → [viewIsAppearing] → [viewDidAppear] → Appeared

Interrupted 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
    viewDidAppear
    or
    viewDidDisappear
  • 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
Animation
types to animate UIKit and AppKit views. This provides access to the full suite of SwiftUI animations, including custom animations.
iOS 18允许使用SwiftUI
Animation
类型为UIKit和AppKit视图添加动画。这让你可以使用全套SwiftUI动画,包括自定义动画。

API 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 APINew SwiftUI Animation API
Generates a
CAAnimation
No
CAAnimation
generated
Animation added to layerAnimates presentation values directly
Animation in layer's
animations
dict
Presentation values in presentation layer
Both approaches reflect values in the presentation layer, but the mechanism differs.
重要架构差异
旧UIKit API新SwiftUI Animation API
生成
CAAnimation
不生成
CAAnimation
动画添加到图层直接为展示值添加动画
动画在图层的
animations
字典中
展示值在展示图层中
两种方式都会在展示图层中反映值,但机制不同。

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
context.animate()
to bridge SwiftUI animations:
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
        }
    }
}
使用
context.animate()
来桥接SwiftUI动画:
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

工作原理

  1. SwiftUI stores animation info in the current
    Transaction
  2. context.animate()
    reads the Transaction's animation
  3. Applies that animation to UIView changes in the closure
  4. If no animation in Transaction, changes happen immediately (no animation)
  1. SwiftUI将动画信息存储在当前的
    Transaction
  2. context.animate()
    读取Transaction中的动画
  3. 将该动画应用到闭包中的UIView变化
  4. 如果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 Int
swift
@State private var count: Int = 0 // ❌ Int无法动画

// 解决方案
@State private var count: Double = 0 // ✅ Double可以动画
Text("\(Int(count))") // 显示为Int

Cause 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)