Timing 动画时序

SwiftUI 中的 Animation 类型描述了值变化的时序曲线,控制值向目标值移动的速度以及在稳定下来之前是否会过冲。我们可以使用 [animation(_:value:)](<https://developer.apple.com/documentation/swiftui/view/animation%28_%3Avalue%3A%29>)  修饰符将其应用于特定视图,或者将状态更改包装在 withAnimation( *: :* ) 中,以便一次性为所有受影响的视图添加动画效果。

Rectangle()
    .frame(width: 100, height: 100)
    .scaleEffect(isExpanded ? 2 : 1)
    .animation(.default, value: isExpanded)

自 iOS 17 起, 默认选项是苹果预设的弹簧动画。如果需要更精细地控制动画时序,我们可以将其替换为其他 Animation 值。本文将介绍 SwiftUI 提供的缓动和弹簧动画,以及用于实现完全自定义时序逻辑的 CustomAnimation 协议。


缓动动画 easing

缓动动画使用三次贝塞尔曲线来描述其时间曲线,该曲线由四个点定义:两个固定端点分别为 (0, 0) 和 (1, 1),以及两个控制点,用于调整它们之间的曲线形状。控制点的位置决定了动画的加速和减速方式,从而赋予每个预设独特的特性。

Screenshot - 2026-06-11 08.22.41.png

SwiftUI 提供了四个用于缓动动画的预设,每个预设都对应一个特定的控制点配置。每个预设都以静态属性的形式提供,并带有默认持续时间;同时,它们也以静态方法的形式提供,该方法接受以秒为单位的显式 duration 参数。

linear 预设

线性预设将两个控制点直接放置在曲线的端点上,这意味着它们对曲线没有任何影响,从而产生恒定的斜率。动画从开始到结束以相同的速度移动,这在大多数界面中显得有些机械,但对于像持续旋转的指示器这类需要保持匀速的场景来说,却是合适的选择。

Screenshot - 2026-06-11 08.43.14.png

Rectangle()
    .frame(width: 250, height: 250)
    .rotationEffect(.degrees(isAnimating ? 360 : 0))
    .animation(.linear(duration: 6), value: isAnimating)

easeIn 预设

easeIn 预设会将第一个控制点向右移动,使曲线的起始部分变平缓。动画开始时速度较慢,然后逐渐加速直至结束,这非常适合即将离开屏幕的元素。

Screenshot - 2026-06-11 08.24.27.png

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : 340)
    .animation(.easeIn(duration: 2), value: isVisible)

easeOut 预设

easeOut 预设会将第二个控制点向左移动,使末端变平。动画快速开始,然后逐渐减速直至停止。这是元素进入或稳定到最终位置时最常用的预设。

Screenshot - 2026-06-11 08.24.37.png

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.easeOut(duration: 2), value: isVisible)

easeInOut 预设

easeInOut 预设会将两个控制点都向内移动,从而形成两端平缓、中间陡峭的曲线。它非常适合在屏幕上两个位置之间移动的元素,因为元素的离开和到达都需要更柔和的边缘。在 iOS 17 之前,这是 SwiftUI 的默认设置。

Screenshot - 2026-06-11 08.24.44.png

Rectangle()
    .frame(width: 250, height: 250)
    .offset(y: hasMoved ? 200 : -200)
    .animation(.easeInOut(duration: 2), value: hasMoved)

自定义缓动 timingCurve

当四个预设曲线无法生成我们所需的精确曲线时, [timingCurve(_:duration:)](https://developer.apple.com/documentation/swiftui/animation/timingcurve(_%3Aduration%3A)) 方法允许我们指定自定义曲线。自 iOS 17 起,该方法接受一个 UnitCurve 值,该类型表示独立于任何动画的时间曲线。

我们使用 bezier(startControlPoint:endControlPoint:) 创建自定义曲线,传入两个 UnitPoint 值来定义三次贝塞尔曲线的控制点。起始控制点决定曲线如何离开原点,结束控制点决定曲线如何到达终点。

Screenshot - 2026-06-11 08.28.24.png

Rectangle()
    .frame(width: 250, height: 250)
    .offset(y: hasMoved ? 200 : -200)
    .animation(
        .timingCurve(.bezier(
            startControlPoint: UnitPoint(x: 0.2, y: 0.8),
            endControlPoint: UnitPoint(x: 0.8, y: 0.2)
        ), duration: 2),
        value: hasMoved
    )

在 iOS 17 之前,可以使用 timingCurve(::::duration:) 实现相同的结果,它直接将四个控制点值作为原始 Double 参数接收。


UnitCurve 还自带一些命名预设,我们可以直接将它们传递给 timingCurve(_:duration:) 。除了上面这些预设之外, UnitCurve 还提供了三种圆形缓入缓出 (circularEaseIn) 、 缓出缓出 (circularEaseOut) 和缓入缓出缓出 (circularEaseInOut) 的变体。与基于贝塞尔曲线的控制点不同,这些圆形变体使用四分之一圆弧作为其基本形状。这使得加速和减速的效果比贝塞尔曲线更加灵敏,在实际应用中感觉更加迅捷。

Screenshot - 2026-06-11 08.29.18.png

Rectangle()
    .frame(width: 250, height: 250)
    .offset(y: hasMoved ? 200 : -200)
    .animation(
        .timingCurve(.circularEaseInOut, duration: 2),
        value: hasMoved
    )

弹性动画 spring

与遵循固定数学曲线的缓动动画不同,弹簧动画模拟的是连接在弹簧上的物理物体。在每一帧中,SwiftUI 都会施加两个力:弹簧力将物体拉向目标位置,而阻尼力则阻碍其运动。

这些力之间的平衡决定了动画的特性。当阻尼力足够强时,物体能够平稳地到达目标位置而不会过冲。当阻尼力较弱时,物体会过冲,并在最终稳定下来之前来回摆动。SwiftUI 提供了三个预设值: smooth 、 snappy 和 bouncy ,分别对应于这个范围内的特定点。

Screenshot - 2026-06-11 08.30.18.png

smooth 预设

平滑预设就像一个临界阻尼弹簧:阻尼力恰到好处,既能防止任何过冲,又能使物体以最快的速度到达目标位置,避免越界,从而实现平稳流畅的到达效果。

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.smooth, value: isVisible)

snappy 预设

快速预设模式的阻尼略微不足,会在稳定下来之前产生一个小的过冲。这种微妙的弹性使其听起来比 smooth 更活泼。

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.snappy, value: isVisible)

bouncy 预设

弹性预设的阻尼较小,会产生清晰可见的过冲和短暂的振荡,然后才稳定下来。它突出了运动本身,适合营造活泼、充满活力的氛围。

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(.bouncy, value: isVisible)

自定义弹性 spring

SwiftUI 提供了多种自定义弹簧动画特性的方法,从调整内置预设到直接指定弹簧参数。

前面提到的三种弹簧预设都对应一个静态方法,该方法接受一个 duration 和 extraBounce 参数。duration duration 控制弹簧回弹所需的时间,而 extraBounce 则在预设的默认弹跳值基础上增加额外的弹跳力。较小的正值会使预设的弹跳力略微增强,而较大的正值则会使 bouncy 力更加强劲。

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : -340)
    .animation(
        .bouncy(duration: 0.5, extraBounce: 0.2),
        value: isVisible
    )

CustomAnimation 自定义动画

如果无法通过内置的定时选项实现所需的动画效果,我们可以使用 iOS 17 及更高版本提供的 CustomAnimation 协议来自行实现定时逻辑。该协议包含一个必需的方法  [animate(value:time:context:)](https://developer.apple.com/documentation/swiftui/customanimation/animate(value%3Atime%3Acontext%3A)) ,SwiftUI 会在每一帧调用此方法,传入要动画化的值和自动画开始以来经过的时间,并期望返回当前时刻的插值值;如果动画结束,则返回 nil 。

以下示例使用 CustomAnimation 来实现弹弓曲线,其中视图在射向目标之前会短暂地向后拉动。

struct SlingshotAnimation: CustomAnimation {

    let duration: TimeInterval = 0.6

    func animate<V: VectorArithmetic>(
        value: V,
        time: TimeInterval,
        context: inout AnimationContext<V>
    ) -> V? {
        guard time <= duration else { return nil }
        let t = time / duration
        let curve = t * t * (3 - 2 * t) + sin(t * .pi) * -0.3
        return value.scaled(by: curve)
    }
}

extension Animation {
    static var slingshot: Animation {
        Animation(SlingshotAnimation())
    }
}

将符合规范的类型包装在 Animation 扩展中,即可将其作为静态属性使用,这与内置预设使用的模式相同。

Rectangle()
    .frame(width: 250, height: 250)
    .offset(x: isVisible ? 0 : 340)
    .animation(.slingshot, value: isVisible)

A rectangle dismissing off screen with a slingshot curve, briefly pulling back before shooting away