SwiftUI

Animations

SwiftUI 的动画系统通过插值(Interpolation)在视图的两个状态之间创建平滑过渡。它深度集成在声明式语法中,使得添加复杂的动画效果变得简单直观。

SwiftUI 的动画系统通过插值(Interpolation)在视图的两个状态之间创建平滑过渡。它深度集成在声明式语法中,使得添加复杂的动画效果变得简单直观。

动画基础(Creating Animations)·

在 SwiftUI 中,动画主要通过两种方式触发:隐式动画显式动画

隐式动画(Implicit Animation)

使用 .animation(_:value:) 修饰符。当指定的 value 发生变化时,SwiftUI 会自动为该视图及其子视图的属性变化应用动画。

struct ImplicitAnimationExample: View {
    @State private var scale = 1.0

    var body: some View {
        Button("Scale Me") {
            scale += 0.5
        }
        .scaleEffect(scale)
        .animation(.default, value: scale) // 仅当 scale 变化时触发动画
    }
}

显式动画 (Explicit Animation)

使用 withAnimation 函数。在闭包内修改状态,所有受该状态影响的视图变化都会被动画化。这种方式提供了更细粒度的控制。

struct ExplicitAnimationExample: View {
    @State private var opacity = 1.0

    var body: some View {
        Button("Fade") {
            withAnimation(.easeIn(duration: 1.0)) {
                opacity = 0.0
            }
        }
        .opacity(opacity)
    }
}

动画类型(Animation Types)

SwiftUI 提供了多种预定义的动画曲线,适用于不同的场景。

标准动画 (Standard)

基于时间曲线的动画,行为可预测。

  • .linear: 匀速运动,机械感强。
  • .easeIn: 慢进快出,适合物体移出屏幕。
  • .easeOut: 快进慢出,适合物体移入屏幕。
  • .easeInOut: 慢进慢出,最自然的通用选择。
.animation(.easeInOut(duration: 0.5), value: isVisible)

弹簧动画 (Spring) - iOS 17+

iOS 17 引入了更具表现力的弹簧动画 API,基于物理模型,通过 duration(持续时间)和 bounce(弹性)来配置。

  • .spring: 通用弹簧动画。
  • .bouncy: 高弹性,生动活泼。
  • .smooth: 无弹性,平滑自然。
  • .snappy: 小弹性,快速响应。
// iOS 17+
.animation(.bouncy(duration: 0.5, extraBounce: 0.2), value: offset)
.animation(.smooth, value: offset)

对于旧版本或更底层的控制,仍可使用 .interpolatingSpring(mass:stiffness:damping:)

自定义曲线 (Custom)

使用贝塞尔曲线定义动画速度。

.animation(.timingCurve(0.2, 0.8, 0.2, 1.0, duration: 1.0), value: offset)

动画配置(Configuration)

可以通过修饰符进一步调整动画的行为。

  • .delay(0.5): 延迟动画开始。
  • .speed(2.0): 加速或减速动画(不改变曲线形状)。
  • .repeatCount(3, autoreverses: true): 重复指定次数。
  • .repeatForever(autoreverses: true): 无限重复。
Circle()
    .scaleEffect(isPulsing ? 1.2 : 1.0)
    .animation(
        .easeInOut(duration: 1.0)
        .repeatForever(autoreverses: true),
        value: isPulsing
    )

事务(Transactions)

Transaction 是 SwiftUI 状态更新的上下文,它携带了动画信息。我们可以通过它来禁用动画或覆盖现有的动画。

禁用动画

var transaction = Transaction()
transaction.disablesAnimations = true

withTransaction(transaction) {
    // 这里的状态改变不会触发动画
    isExpanded.toggle()
}

或者使用 .transaction 修饰符:

Toggle("Toggle", isOn: $isOn)
    .transaction { transaction in
        transaction.animation = nil // 禁用此视图的动画
    }

阶段动画(Phase Animations) - iOS 17+

PhaseAnimator 允许视图在一系列离散的阶段(Phases)之间循环动画。非常适合创建多步骤的动画序列。

enum Phase: CaseIterable {
    case initial, move, scale
}

PhaseAnimator(Phase.allCases, trigger: startAnimation) { phase in
    Circle()
        .foregroundStyle(.blue)
        // 根据当前 phase 定义视图状态
        .scaleEffect(phase == .scale ? 1.5 : 1.0)
        .offset(x: phase == .move ? 100 : 0)
} animation: { phase in
    // 为每个 phase 转换定义不同的动画
    switch phase {
    case .initial: .bouncy
    case .move: .smooth(duration: 1.0)
    case .scale: .spring(duration: 0.5)
    }
}

关键帧动画(Keyframe Animations)- iOS 17+

KeyframeAnimator 提供了最强大的控制力,允许独立控制多个属性的动画轨道(Tracks),并使用关键帧定义每一帧的值。

struct KeyframeExample: View {
    @State private var runAnimation = false

    var body: some View {
        KeyframeAnimator(
            initialValue: AnimationValues(),
            trigger: runAnimation
        ) { values in
            // 使用计算出的 values 更新视图
            Circle()
                .scaleEffect(values.scale)
                .offset(y: values.verticalOffset)
                .opacity(values.opacity)
        } keyframes: { _ in
            // 定义多个轨道
            KeyframeTrack(\.scale) {
                CubicKeyframe(1.2, duration: 0.2)
                LinearKeyframe(1.0, duration: 0.5)
            }
            
            KeyframeTrack(\.verticalOffset) {
                SpringKeyframe(-50, duration: 0.3)
                SpringKeyframe(0, duration: 0.3)
            }
            
            KeyframeTrack(\.opacity) {
                LinearKeyframe(0.5, duration: 0.2)
                LinearKeyframe(1.0, duration: 0.4)
            }
        }
        .onTapGesture { runAnimation.toggle() }
    }
}

struct AnimationValues {
    var scale = 1.0
    var verticalOffset = 0.0
    var opacity = 1.0
}

自定义动画数据(Animatable)

当需要动画化非标准属性(如绘制路径中的点)时,需要遵循 Animatable 协议。核心是实现 animatableData 属性,该属性必须遵循 VectorArithmetic

SwiftUI 会在动画过程中多次设置 animatableData 的值,从而驱动视图重绘。

struct AnimatableCorner: Shape {
    var cornerRadius: CGFloat

    // 告诉 SwiftUI 这个属性是可以动画的
    var animatableData: CGFloat {
        get { cornerRadius }
        set { cornerRadius = newValue }
    }

    func path(in rect: CGRect) -> Path {
        Path { path in
            path.addRoundedRect(in: rect, cornerSize: CGSize(width: cornerRadius, height: cornerRadius))
        }
    }
}

// 使用
// withAnimation { cornerRadius = 50 }
// AnimatableCorner(cornerRadius: cornerRadius)

对于多个属性,可以使用 AnimatablePair 组合。

var animatableData: AnimatablePair<CGFloat, CGFloat> {
    get { AnimatablePair(firstValue, secondValue) }
    set {
        firstValue = newValue.first
        secondValue = newValue.second
    }
}
在 GitHub 上编辑

上次更新于