swiftui-animation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Animation (iOS 26+)

SwiftUI 动画(iOS 26+)

Review, write, and fix SwiftUI animations. Apply modern animation APIs with correct timing, transitions, and accessibility handling using Swift 6.2 patterns.
审查、编写和修复SwiftUI动画。使用Swift 6.2的模式,应用具有正确时序、转场效果和无障碍处理的现代动画API。

Triage Workflow

问题排查流程

Step 1: Identify the animation category

步骤1:识别动画类别

CategoryAPIWhen to use
State-driven
withAnimation
,
.animation(_:value:)
Simple property changes
Multi-phase
PhaseAnimator
Sequenced multi-step animations
Keyframe
KeyframeAnimator
Complex multi-property choreography
Shared element
matchedGeometryEffect
Layout-driven hero transitions
Navigation
matchedTransitionSource
+
.navigationTransition(.zoom)
NavigationStack push/pop zoom
View lifecycle
.transition()
Insertion and removal
Text content
.contentTransition()
In-place text/number changes
Symbol
.symbolEffect()
SF Symbol animations
Custom
CustomAnimation
protocol
Novel timing curves
类别API使用场景
状态驱动
withAnimation
,
.animation(_:value:)
简单属性变更
多阶段
PhaseAnimator
序列式多步骤动画
关键帧
KeyframeAnimator
复杂多属性协同动画
共享元素
matchedGeometryEffect
布局驱动的共享元素转场
导航
matchedTransitionSource
+
.navigationTransition(.zoom)
NavigationStack 推入/弹出缩放效果
视图生命周期
.transition()
视图插入与移除
文本内容
.contentTransition()
文本/数字原地变更
图标
.symbolEffect()
SF Symbol 动画
自定义
CustomAnimation
协议
自定义时序曲线

Step 2: Choose the animation curve

步骤2:选择动画曲线

swift
// Timing curves
.linear                              // constant speed
.easeIn(duration: 0.3)              // slow start
.easeOut(duration: 0.3)             // slow end
.easeInOut(duration: 0.3)           // slow start and end

// Spring presets (preferred for natural motion)
.smooth                              // no bounce, fluid
.smooth(duration: 0.5, extraBounce: 0.0)
.snappy                              // small bounce, responsive
.snappy(duration: 0.4, extraBounce: 0.1)
.bouncy                              // visible bounce, playful
.bouncy(duration: 0.5, extraBounce: 0.2)

// Custom spring
.spring(duration: 0.5, bounce: 0.3, blendDuration: 0.0)
.spring(Spring(duration: 0.6, bounce: 0.2), blendDuration: 0.0)
.interactiveSpring(response: 0.15, dampingFraction: 0.86)
swift
// 时序曲线
.linear                              // 匀速
.easeIn(duration: 0.3)              // 慢启动
.easeOut(duration: 0.3)             // 慢结束
.easeInOut(duration: 0.3)           // 慢启动+慢结束

// 弹簧预设(自然动效首选)
.smooth                              // 无弹跳,流畅
.smooth(duration: 0.5, extraBounce: 0.0)
.snappy                              // 小幅弹跳,响应迅速
.snappy(duration: 0.4, extraBounce: 0.1)
.bouncy                              // 明显弹跳,活泼
.bouncy(duration: 0.5, extraBounce: 0.2)

// 自定义弹簧
.spring(duration: 0.5, bounce: 0.3, blendDuration: 0.0)
.spring(Spring(duration: 0.6, bounce: 0.2), blendDuration: 0.0)
.interactiveSpring(response: 0.15, dampingFraction: 0.86)

Step 3: Apply and verify

步骤3:应用与验证

  • Confirm animation triggers on the correct state change.
  • Test with Accessibility > Reduce Motion enabled.
  • Verify no expensive work runs inside animation content closures.
  • 确认动画触发正确的状态变更。
  • 测试开启“辅助功能 > 减少动态效果”后的表现。
  • 确保动画内容闭包内没有执行高开销操作。

withAnimation (Explicit Animation)

withAnimation(显式动画)

swift
withAnimation(.spring) { isExpanded.toggle() }

// With completion (iOS 17+)
withAnimation(.smooth(duration: 0.35), completionCriteria: .logicallyComplete) {
    isExpanded = true
} completion: { loadContent() }
swift
withAnimation(.spring) { isExpanded.toggle() }

// 带完成回调(iOS 17+)
withAnimation(.smooth(duration: 0.35), completionCriteria: .logicallyComplete) {
    isExpanded = true
} completion: { loadContent() }

.animation(_:value:) (Implicit Animation)

.animation(_:value:)(隐式动画)

swift
Circle()
    .scaleEffect(isActive ? 1.2 : 1.0)
    .opacity(isActive ? 1.0 : 0.6)
    .animation(.bouncy, value: isActive)
swift
Circle()
    .scaleEffect(isActive ? 1.2 : 1.0)
    .opacity(isActive ? 1.0 : 0.6)
    .animation(.bouncy, value: isActive)

Spring Type (iOS 17+)

Spring 类型(iOS 17+)

Four initializer forms for different mental models.
swift
// Perceptual (preferred)
Spring(duration: 0.5, bounce: 0.3)

// Physical
Spring(mass: 1.0, stiffness: 100.0, damping: 10.0)

// Response-based
Spring(response: 0.5, dampingRatio: 0.7)

// Settling-based
Spring(settlingDuration: 1.0, dampingRatio: 0.8)
Three presets mirror Animation presets:
.smooth
,
.snappy
,
.bouncy
.
四种初始化方式,适配不同的思维模型。
swift
// 感知优先(推荐)
Spring(duration: 0.5, bounce: 0.3)

// 物理参数
Spring(mass: 1.0, stiffness: 100.0, damping: 10.0)

// 响应时间
Spring(response: 0.5, dampingRatio: 0.7)

// 稳定时间
Spring(settlingDuration: 1.0, dampingRatio: 0.8)
三种预设与Animation预设对应:
.smooth
,
.snappy
,
.bouncy

PhaseAnimator (iOS 17+)

PhaseAnimator(iOS 17+)

Cycle through discrete phases with per-phase animation curves.
swift
enum PulsePhase: CaseIterable {
    case idle, grow, shrink
}

struct PulsingDot: View {
    var body: some View {
        PhaseAnimator(PulsePhase.allCases) { phase in
            Circle()
                .frame(width: 40, height: 40)
                .scaleEffect(phase == .grow ? 1.4 : 1.0)
                .opacity(phase == .shrink ? 0.5 : 1.0)
        } animation: { phase in
            switch phase {
            case .idle: .easeIn(duration: 0.2)
            case .grow: .spring(duration: 0.4, bounce: 0.3)
            case .shrink: .easeOut(duration: 0.3)
            }
        }
    }
}
Trigger-based variant runs one cycle per trigger change:
swift
PhaseAnimator(PulsePhase.allCases, trigger: tapCount) { phase in
    // ...
} animation: { _ in .spring(duration: 0.4) }
循环切换离散阶段,每个阶段可配置独立的动画曲线。
swift
enum PulsePhase: CaseIterable {
    case idle, grow, shrink
}

struct PulsingDot: View {
    var body: some View {
        PhaseAnimator(PulsePhase.allCases) { phase in
            Circle()
                .frame(width: 40, height: 40)
                .scaleEffect(phase == .grow ? 1.4 : 1.0)
                .opacity(phase == .shrink ? 0.5 : 1.0)
        } animation: { phase in
            switch phase {
            case .idle: .easeIn(duration: 0.2)
            case .grow: .spring(duration: 0.4, bounce: 0.3)
            case .shrink: .easeOut(duration: 0.3)
            }
        }
    }
}
触发式变体:每次触发器变更时运行一轮循环:
swift
PhaseAnimator(PulsePhase.allCases, trigger: tapCount) { phase in
    // ...
} animation: { _ in .spring(duration: 0.4) }

KeyframeAnimator (iOS 17+)

KeyframeAnimator(iOS 17+)

Animate multiple properties along independent timelines.
swift
struct AnimValues {
    var scale: Double = 1.0
    var yOffset: Double = 0.0
    var opacity: Double = 1.0
}

struct BounceView: View {
    @State private var trigger = false

    var body: some View {
        Image(systemName: "star.fill")
            .font(.largeTitle)
            .keyframeAnimator(
                initialValue: AnimValues(),
                trigger: trigger
            ) { content, value in
                content
                    .scaleEffect(value.scale)
                    .offset(y: value.yOffset)
                    .opacity(value.opacity)
            } keyframes: { _ in
                KeyframeTrack(\.scale) {
                    SpringKeyframe(1.5, duration: 0.3)
                    CubicKeyframe(1.0, duration: 0.4)
                }
                KeyframeTrack(\.yOffset) {
                    CubicKeyframe(-30, duration: 0.2)
                    CubicKeyframe(0, duration: 0.4)
                }
                KeyframeTrack(\.opacity) {
                    LinearKeyframe(0.6, duration: 0.15)
                    LinearKeyframe(1.0, duration: 0.25)
                }
            }
            .onTapGesture { trigger.toggle() }
    }
}
Keyframe types:
LinearKeyframe
(linear),
CubicKeyframe
(smooth curve),
SpringKeyframe
(spring physics),
MoveKeyframe
(instant jump).
Use
repeating: true
for looping keyframe animations.
沿独立时间线为多个属性设置动画。
swift
struct AnimValues {
    var scale: Double = 1.0
    var yOffset: Double = 0.0
    var opacity: Double = 1.0
}

struct BounceView: View {
    @State private var trigger = false

    var body: some View {
        Image(systemName: "star.fill")
            .font(.largeTitle)
            .keyframeAnimator(
                initialValue: AnimValues(),
                trigger: trigger
            ) { content, value in
                content
                    .scaleEffect(value.scale)
                    .offset(y: value.yOffset)
                    .opacity(value.opacity)
            } keyframes: { _ in
                KeyframeTrack(\.scale) {
                    SpringKeyframe(1.5, duration: 0.3)
                    CubicKeyframe(1.0, duration: 0.4)
                }
                KeyframeTrack(\.yOffset) {
                    CubicKeyframe(-30, duration: 0.2)
                    CubicKeyframe(0, duration: 0.4)
                }
                KeyframeTrack(\.opacity) {
                    LinearKeyframe(0.6, duration: 0.15)
                    LinearKeyframe(1.0, duration: 0.25)
                }
            }
            .onTapGesture { trigger.toggle() }
    }
}
关键帧类型:
LinearKeyframe
(线性)、
CubicKeyframe
(平滑曲线)、
SpringKeyframe
(弹簧物理)、
MoveKeyframe
(瞬间跳转)。
设置
repeating: true
可实现循环关键帧动画。

@Animatable Macro

@Animatable 宏

Replaces manual
AnimatableData
boilerplate. Attach to any type with animatable stored properties.
swift
// WRONG: Manual AnimatableData (verbose, error-prone)
struct WaveShape: Shape, Animatable {
    var frequency: Double
    var amplitude: Double
    var phase: Double

    var animatableData: AnimatablePair<Double, AnimatablePair<Double, Double>> {
        get { AnimatablePair(frequency, AnimatablePair(amplitude, phase)) }
        set {
            frequency = newValue.first
            amplitude = newValue.second.first
            phase = newValue.second.second
        }
    }
    // ...
}

// CORRECT: @Animatable macro synthesizes animatableData
@Animatable
struct WaveShape: Shape {
    var frequency: Double
    var amplitude: Double
    var phase: Double
    @AnimatableIgnored var lineWidth: CGFloat

    func path(in rect: CGRect) -> Path {
        // draw wave using frequency, amplitude, phase
    }
}
Rules:
  • Stored properties must conform to
    VectorArithmetic
    .
  • Use
    @AnimatableIgnored
    to exclude non-animatable properties.
  • Computed properties are never included.
替代手动编写
AnimatableData
的冗余代码。可附加到任何包含可动画存储属性的类型。
swift
// 错误写法:手动编写AnimatableData(冗长且易出错)
struct WaveShape: Shape, Animatable {
    var frequency: Double
    var amplitude: Double
    var phase: Double

    var animatableData: AnimatablePair<Double, AnimatablePair<Double, Double>> {
        get { AnimatablePair(frequency, AnimatablePair(amplitude, phase)) }
        set {
            frequency = newValue.first
            amplitude = newValue.second.first
            phase = newValue.second.second
        }
    }
    // ...
}

// 正确写法:@Animatable宏自动生成animatableData
@Animatable
struct WaveShape: Shape {
    var frequency: Double
    var amplitude: Double
    var phase: Double
    @AnimatableIgnored var lineWidth: CGFloat

    func path(in rect: CGRect) -> Path {
        // 使用frequency、amplitude、phase绘制波形
    }
}
规则:
  • 存储属性必须遵循
    VectorArithmetic
    协议。
  • 使用
    @AnimatableIgnored
    排除不可动画的属性。
  • 计算属性永远不会被包含。

matchedGeometryEffect (iOS 14+)

matchedGeometryEffect(iOS 14+)

Synchronize geometry between views for shared-element animations.
swift
struct HeroView: View {
    @Namespace private var heroSpace
    @State private var isExpanded = false

    var body: some View {
        if isExpanded {
            DetailCard()
                .matchedGeometryEffect(id: "card", in: heroSpace)
                .onTapGesture {
                    withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
                        isExpanded = false
                    }
                }
        } else {
            ThumbnailCard()
                .matchedGeometryEffect(id: "card", in: heroSpace)
                .onTapGesture {
                    withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
                        isExpanded = true
                    }
                }
        }
    }
}
Exactly one view per ID must be visible at a time for the interpolation to work.
同步不同视图的几何信息,实现共享元素动画。
swift
struct HeroView: View {
    @Namespace private var heroSpace
    @State private var isExpanded = false

    var body: some View {
        if isExpanded {
            DetailCard()
                .matchedGeometryEffect(id: "card", in: heroSpace)
                .onTapGesture {
                    withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
                        isExpanded = false
                    }
                }
        } else {
            ThumbnailCard()
                .matchedGeometryEffect(id: "card", in: heroSpace)
                .onTapGesture {
                    withAnimation(.spring(duration: 0.4, bounce: 0.2)) {
                        isExpanded = true
                    }
                }
        }
    }
}
同一时间每个ID只能对应一个可见视图,才能保证插值效果正常工作。

Navigation Zoom Transition (iOS 18+)

导航缩放转场(iOS 18+)

Pair
matchedTransitionSource
on the source view with
.navigationTransition(.zoom(...))
on the destination.
swift
struct GalleryView: View {
    @Namespace private var zoomSpace
    let items: [GalleryItem]

    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
                    ForEach(items) { item in
                        NavigationLink {
                            GalleryDetail(item: item)
                                .navigationTransition(
                                    .zoom(sourceID: item.id, in: zoomSpace)
                                )
                        } label: {
                            ItemThumbnail(item: item)
                                .matchedTransitionSource(
                                    id: item.id, in: zoomSpace
                                )
                        }
                    }
                }
            }
        }
    }
}
Apply
.navigationTransition
on the destination view, not on inner containers.
在源视图上使用
matchedTransitionSource
,在目标视图上搭配
.navigationTransition(.zoom(...))
swift
struct GalleryView: View {
    @Namespace private var zoomSpace
    let items: [GalleryItem]

    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))]) {
                    ForEach(items) { item in
                        NavigationLink {
                            GalleryDetail(item: item)
                                .navigationTransition(
                                    .zoom(sourceID: item.id, in: zoomSpace)
                                )
                        } label: {
                            ItemThumbnail(item: item)
                                .matchedTransitionSource(
                                    id: item.id, in: zoomSpace
                                )
                        }
                    }
                }
            }
        }
    }
}
.navigationTransition
应应用在目标视图上,而非内部容器。

Transitions (iOS 17+)

转场效果(iOS 17+)

Control how views animate on insertion and removal.
swift
if showBanner {
    BannerView()
        .transition(.move(edge: .top).combined(with: .opacity))
}
Built-in types:
.opacity
,
.slide
,
.scale
,
.scale(_:anchor:)
,
.move(edge:)
,
.push(from:)
,
.offset(x:y:)
,
.identity
,
.blurReplace
,
.blurReplace(_:)
,
.symbolEffect
,
.symbolEffect(_:options:)
.
Asymmetric transitions:
swift
.transition(.asymmetric(
    insertion: .push(from: .bottom),
    removal: .opacity
))
控制视图插入和移除时的动画效果。
swift
if showBanner {
    BannerView()
        .transition(.move(edge: .top).combined(with: .opacity))
}
内置类型:
.opacity
,
.slide
,
.scale
,
.scale(_:anchor:)
,
.move(edge:)
,
.push(from:)
,
.offset(x:y:)
,
.identity
,
.blurReplace
,
.blurReplace(_:)
,
.symbolEffect
,
.symbolEffect(_:options:)
不对称转场:
swift
.transition(.asymmetric(
    insertion: .push(from: .bottom),
    removal: .opacity
))

ContentTransition (iOS 16+)

ContentTransition(iOS 16+)

Animate in-place content changes without insertion/removal.
swift
Text("\(score)")
    .contentTransition(.numericText(countsDown: false))
    .animation(.snappy, value: score)

// For SF Symbols
Image(systemName: isMuted ? "speaker.slash" : "speaker.wave.3")
    .contentTransition(.symbolEffect(.replace.downUp))
Types:
.identity
,
.interpolate
,
.opacity
,
.numericText(countsDown:)
,
.numericText(value:)
,
.symbolEffect
.
无需插入/移除视图,即可为原地内容变更设置动画。
swift
Text("\(score)")
    .contentTransition(.numericText(countsDown: false))
    .animation(.snappy, value: score)

// SF Symbol 示例
Image(systemName: isMuted ? "speaker.slash" : "speaker.wave.3")
    .contentTransition(.symbolEffect(.replace.downUp))
类型:
.identity
,
.interpolate
,
.opacity
,
.numericText(countsDown:)
,
.numericText(value:)
,
.symbolEffect

Symbol Effects (iOS 17+)

Symbol 效果(iOS 17+)

Animate SF Symbols with semantic effects.
swift
// Discrete (triggers on value change)
Image(systemName: "bell.fill")
    .symbolEffect(.bounce, value: notificationCount)

Image(systemName: "arrow.clockwise")
    .symbolEffect(.wiggle.clockwise, value: refreshCount)

// Indefinite (active while condition holds)
Image(systemName: "wifi")
    .symbolEffect(.pulse, isActive: isSearching)

Image(systemName: "mic.fill")
    .symbolEffect(.breathe, isActive: isRecording)

// Variable color with chaining
Image(systemName: "speaker.wave.3.fill")
    .symbolEffect(
        .variableColor.iterative.reversing.dimInactiveLayers,
        options: .repeating,
        isActive: isPlaying
    )
All effects:
.bounce
,
.pulse
,
.variableColor
,
.scale
,
.appear
,
.disappear
,
.replace
,
.breathe
,
.rotate
,
.wiggle
.
Scope:
.byLayer
,
.wholeSymbol
. Direction varies per effect.
为SF Symbol添加语义化动画。
swift
// 离散触发(值变更时触发)
Image(systemName: "bell.fill")
    .symbolEffect(.bounce, value: notificationCount)

Image(systemName: "arrow.clockwise")
    .symbolEffect(.wiggle.clockwise, value: refreshCount)

// 持续动画(条件满足时激活)
Image(systemName: "wifi")
    .symbolEffect(.pulse, isActive: isSearching)

Image(systemName: "mic.fill")
    .symbolEffect(.breathe, isActive: isRecording)

// 可变颜色与链式调用
Image(systemName: "speaker.wave.3.fill")
    .symbolEffect(
        .variableColor.iterative.reversing.dimInactiveLayers,
        options: .repeating,
        isActive: isPlaying
    )
所有效果:
.bounce
,
.pulse
,
.variableColor
,
.scale
,
.appear
,
.disappear
,
.replace
,
.breathe
,
.rotate
,
.wiggle
作用范围:
.byLayer
,
.wholeSymbol
。方向因效果而异。

Common Mistakes

常见错误

1. Animating without a value binding

1. 动画未绑定值

swift
// WRONG: .animation without value triggers on any state change
Text("Hello")
    .opacity(isVisible ? 1 : 0)
    .animation(.easeIn)

// CORRECT: Bind to the specific value
Text("Hello")
    .opacity(isVisible ? 1 : 0)
    .animation(.easeIn, value: isVisible)
swift
// 错误:无value的.animation会在任意状态变更时触发
Text("Hello")
    .opacity(isVisible ? 1 : 0)
    .animation(.easeIn)

// 正确:绑定到特定值
Text("Hello")
    .opacity(isVisible ? 1 : 0)
    .animation(.easeIn, value: isVisible)

2. Expensive work inside animation closures

2. 动画闭包内执行高开销操作

swift
// WRONG: Heavy computation every frame
.keyframeAnimator(initialValue: vals, trigger: t) { content, value in
    let filtered = applyExpensiveFilter(content)  // runs every frame
    return filtered.opacity(value.opacity)
} keyframes: { _ in /* ... */ }

// CORRECT: Precompute outside, animate only visual properties
.keyframeAnimator(initialValue: vals, trigger: t) { content, value in
    content.opacity(value.opacity)
} keyframes: { _ in /* ... */ }
swift
// 错误:每帧都执行重计算
.keyframeAnimator(initialValue: vals, trigger: t) { content, value in
    let filtered = applyExpensiveFilter(content)  // 每帧运行
    return filtered.opacity(value.opacity)
} keyframes: { _ in /* ... */ }

// 正确:提前计算,仅动画视觉属性
.keyframeAnimator(initialValue: vals, trigger: t) { content, value in
    content.opacity(value.opacity)
} keyframes: { _ in /* ... */ }

3. Missing reduce motion support

3. 未支持减少动态效果

swift
// WRONG: Ignores accessibility setting
withAnimation(.bouncy) { showDetail = true }

// CORRECT: Respect reduce motion
@Environment(\.accessibilityReduceMotion) private var reduceMotion

withAnimation(reduceMotion ? .none : .bouncy) { showDetail = true }
swift
// 错误:忽略辅助功能设置
withAnimation(.bouncy) { showDetail = true }

// 正确:遵循减少动态效果设置
@Environment(\.accessibilityReduceMotion) private var reduceMotion

withAnimation(reduceMotion ? .none : .bouncy) { showDetail = true }

4. Multiple matchedGeometryEffect sources

4. 多个matchedGeometryEffect源视图

swift
// WRONG: Both visible with same ID -- undefined behavior
HStack {
    Circle().matchedGeometryEffect(id: "dot", in: ns)
    Circle().matchedGeometryEffect(id: "dot", in: ns)
}

// CORRECT: Only one source visible at a time via conditional
if onLeft {
    Circle().matchedGeometryEffect(id: "dot", in: ns)
} else {
    Circle().matchedGeometryEffect(id: "dot", in: ns)
}
swift
// 错误:同一ID对应两个可见视图——行为未定义
HStack {
    Circle().matchedGeometryEffect(id: "dot", in: ns)
    Circle().matchedGeometryEffect(id: "dot", in: ns)
}

// 正确:通过条件控制同一时间仅一个源视图可见
if onLeft {
    Circle().matchedGeometryEffect(id: "dot", in: ns)
} else {
    Circle().matchedGeometryEffect(id: "dot", in: ns)
}

5. Using DispatchQueue or UIView.animate

5. 使用DispatchQueue或UIView.animate

swift
// WRONG: UIKit patterns in SwiftUI
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    withAnimation { isVisible = true }
}
UIView.animate(withDuration: 0.3) { /* ... */ }

// CORRECT: SwiftUI animation with delay
withAnimation(.spring.delay(0.5)) { isVisible = true }
withAnimation(.easeInOut(duration: 0.3)) { /* state change */ }
swift
// 错误:在SwiftUI中使用UIKit模式
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
    withAnimation { isVisible = true }
}
UIView.animate(withDuration: 0.3) { /* ... */ }

// 正确:使用SwiftUI动画的延迟参数
withAnimation(.spring.delay(0.5)) { isVisible = true }
withAnimation(.easeInOut(duration: 0.3)) { /* 状态变更 */ }

6. Forgetting animation on ContentTransition

6. ContentTransition未搭配动画

swift
// WRONG: No animation -- content transition has no effect
Text("\(count)")
    .contentTransition(.numericText(countsDown: true))

// CORRECT: Pair with animation modifier
Text("\(count)")
    .contentTransition(.numericText(countsDown: true))
    .animation(.snappy, value: count)
swift
// 错误:无动画——内容转场无效
Text("\(count)")
    .contentTransition(.numericText(countsDown: true))

// 正确:与animation修饰符搭配使用
Text("\(count)")
    .contentTransition(.numericText(countsDown: true))
    .animation(.snappy, value: count)

7. navigationTransition on wrong view

7. navigationTransition应用在错误视图

swift
// WRONG: Applied inside a container
NavigationLink {
    VStack {
        DetailView(item: item)
            .navigationTransition(.zoom(sourceID: item.id, in: ns))
    }
} label: { /* ... */ }

// CORRECT: Applied on the outermost destination view
NavigationLink {
    DetailView(item: item)
        .navigationTransition(.zoom(sourceID: item.id, in: ns))
} label: { /* ... */ }
swift
// 错误:应用在容器内部
NavigationLink {
    VStack {
        DetailView(item: item)
            .navigationTransition(.zoom(sourceID: item.id, in: ns))
    }
} label: { /* ... */ }

// 正确:应用在最外层的目标视图
NavigationLink {
    DetailView(item: item)
        .navigationTransition(.zoom(sourceID: item.id, in: ns))
} label: { /* ... */ }

Review Checklist

审查清单

  • Animation curve matches intent (spring for natural, ease for mechanical)
  • withAnimation
    wraps the state change, not the view
  • .animation(_:value:)
    has an explicit
    value
    parameter
  • matchedGeometryEffect
    has exactly one source per ID at a time
  • Navigation zoom uses matching
    id
    and
    namespace
    on source and destination
  • @Animatable
    macro used instead of manual
    animatableData
  • accessibilityReduceMotion
    is checked and respected
  • No heavy computation inside keyframe/phase content closures
  • No
    DispatchQueue
    or
    UIView.animate
    for animation timing
  • Transitions use
    .transition()
    on conditionally inserted views
  • contentTransition
    is paired with
    .animation(_:value:)
  • Symbol effects use correct category (discrete vs indefinite)
  • Ensure animated state changes happen on @MainActor; types driving animations should be Sendable if passed across concurrency boundaries
  • 动画曲线符合预期(弹簧用于自然动效,缓动用于机械动效)
  • withAnimation
    包裹的是状态变更,而非视图
  • .animation(_:value:)
    包含显式的
    value
    参数
  • matchedGeometryEffect
    同一时间每个ID仅对应一个源视图
  • 导航缩放转场在源视图和目标视图使用匹配的
    id
    namespace
  • 使用
    @Animatable
    宏替代手动编写
    animatableData
  • 检查并遵循
    accessibilityReduceMotion
    设置
  • 关键帧/阶段内容闭包内无重计算操作
  • 未使用
    DispatchQueue
    UIView.animate
    处理动画时序
  • 转场效果通过
    .transition()
    应用在条件插入的视图上
  • contentTransition
    .animation(_:value:)
    搭配使用
  • Symbol效果使用正确的类别(离散触发 vs 持续动画)
  • 确保动画状态变更在@MainActor上执行;若跨并发边界传递,驱动动画的类型应遵循Sendable协议

Reference Material

参考资料

  • See
    references/animation-advanced.md
    for CustomAnimation protocol, full Spring variants, all Transition types, symbol effect details, Transaction system, UnitCurve types, and performance guidance.
  • 查看
    references/animation-advanced.md
    了解CustomAnimation协议、完整的Spring变体、所有Transition类型、Symbol效果细节、Transaction系统、UnitCurve类型及性能优化指南。