motion-system

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Motion system

动效系统

An app feels like one app when the same kind of moment always moves the same way. Most codebases drift: a dozen durations, springs nobody could tell apart, a success haptic used for everything. Your job is to replace that drift with a few named values, grounded in the app's own best motion and in real apps from the
60fps
library.
The deliverable is a proposal, then one new file. Do not edit existing call sites until the user agrees to the proposal.
当同类场景每次都以相同方式运动时,app 才会有统一一致的感觉。大多数代码库都会逐渐产生漂移:十来个 duration、彼此难以分辨的 spring,以及被用于所有场景的同一个成功 haptic。你的任务是用少量具名值替代这种漂移。这些值应以 app 自身最好的动效,以及
60fps
库中真实 app 的动效为依据。
交付物先是一份方案,然后是一个新文件。在用户同意方案之前,不要修改现有调用点。

1. Find the floor

1. 确定最低版本

Read the app target's
IPHONEOS_DEPLOYMENT_TARGET
. When the target sets its own value it overrides the project-wide one, in either direction. Look wherever the project defines it: the
.xcodeproj
, an XcodeGen
project.yml
,
.xcconfig
files, or
Package.swift
. Every API you propose must exist at that version. Note the Swift language mode and any default actor isolation setting too; you need them to compile in step 5.
读取 app target 的
IPHONEOS_DEPLOYMENT_TARGET
。如果 target 设置了自己的值,无论数值高于还是低于项目级设置,它都会覆盖项目级值。检查项目定义该值的所有位置:
.xcodeproj
、XcodeGen 的
project.yml
、
.xcconfig
文件或
Package.swift
。你提出的每个 API 都必须存在于该版本中。还要记录 Swift 语言模式以及默认 actor isolation 设置,第 5 步编译时需要使用这些信息。

2. Inventory what the app does now

2. 盘点 app 当前的动效

Search the code for every motion value and everything timed against one:
  • Animations:
    withAnimation(
    ,
    .animation(
    ,
    .spring(
    ,
    .interactiveSpring
    ,
    .interpolatingSpring
    ,
    .smooth
    ,
    .snappy
    ,
    .bouncy
    ,
    .easeIn
    ,
    .easeOut
    ,
    .easeInOut
    ,
    .linear
    ,
    .default
    ,
    duration:
    ,
    .delay(
    ,
    response:
    ,
    dampingFraction:
    . A bare
    .smooth
    ,
    .snappy
    or
    .bouncy
    is a value too (0.5 s by default), so count it.
  • Transitions and effects:
    .transition(
    ,
    contentTransition
    ,
    matchedGeometryEffect
    ,
    matchedTransitionSource
    ,
    phaseAnimator
    ,
    keyframeAnimator
    ,
    symbolEffect
    .
  • Timing coupled to animation:
    Task.sleep
    ,
    asyncAfter
    , completion handlers. These are the riskiest to change: a sleep tuned to a 0.35 s animation breaks when the animation changes.
  • Haptics:
    sensoryFeedback
    , the
    UI...FeedbackGenerator
    classes, and the call sites of any wrapper the app has around them (a
    HapticManager
    with 50 call sites matters more than the class).
  • Reduce Motion: every way it is read (
    accessibilityReduceMotion
    ,
    isReduceMotionEnabled
    ).
  • Existing helpers: any
    Motion
    ,
    Animation
    or haptics extension or manager.
Group the values by the job they do, not by file:
  • Feedback: a press, a toggle, a selection, a small swap in place. Should be quick.
  • Change: content swapping, a row appearing, a number changing.
  • Arrival: a sheet, a card, a screen element entering.
  • Emphasis: the rare moment that should be felt (a success, a milestone).
  • Haptics: which kind is used for what.
Count the distinct values in each group. That count is the problem statement: "Arrival uses 8 different timings between 0.3 s and 0.7 s". Some motion is not a job and stays out of the system: gesture tracking (parallax, a finger-following drag) and values a comment says are load-bearing. List those as deliberate exceptions.
Extend what exists. If the app has a haptics wrapper (often with an in-app on/off switch), new haptic roles go through it, never around it:
sensoryFeedback
would bypass the switch. If an existing motion helper is broken or unused, say so and propose fixing or removing it, never a second helper beside it.
搜索代码中所有动效值,以及所有以动效时长为基准的 timing:
  • 动画:
    withAnimation(
    、
    .animation(
    、
    .spring(
    、
    .interactiveSpring
    、
    .interpolatingSpring
    、
    .smooth
    、
    .snappy
    、
    .bouncy
    、
    .easeIn
    、
    .easeOut
    、
    .easeInOut
    、
    .linear
    、
    .default
    、
    duration:
    、
    .delay(
    、
    response:
    、
    dampingFraction:
    。单独使用
    .smooth
    、
    .snappy
    或
    .bouncy
    也是一个值,默认时长为 0.5 s,因此也要统计。
  • 转场与效果:
    .transition(
    、
    contentTransition
    、
    matchedGeometryEffect
    、
    matchedTransitionSource
    、
    phaseAnimator
    、
    keyframeAnimator
    、
    symbolEffect
    。
  • 与动画耦合的 timing:
    Task.sleep
    、
    asyncAfter
    、completion handler。这些是改动风险最高的部分:如果某个 sleep 针对 0.35 s 的动画做过调整,那么动画发生变化时它就会失效。
  • 触觉反馈:
    sensoryFeedback
    、
    UI...FeedbackGenerator
    类,以及 app 对这些类所使用的任何 wrapper 的调用点。如果某个
    HapticManager
    有 50 个调用点,它比类本身更重要。
  • Reduce Motion:每一种读取方式,包括
    accessibilityReduceMotion
    、
    isReduceMotionEnabled
    。
  • 现有 helper:任何
    Motion
    、
    Animation
    或 haptic extension 或 manager。
按用途对值进行分组,而不是按文件分组:
  • 反馈(Feedback):按下、切换、选中、原地小幅替换。应当足够快捷。
  • 变化(Change):内容替换、行出现、数字变化。
  • 入场(Arrival):sheet、卡片或屏幕元素进入。
  • 强调(Emphasis):少数应当被明显感知的时刻,例如成功或里程碑。
  • 触觉反馈(Haptics):每种反馈用于什么场景。
统计每组中不同值的数量。这个数量就是需要解决的问题,例如:“Arrival 使用了 8 个不同的 timing,范围为 0.3 s 到 0.7 s”。有些动效不属于某项用途,应排除在系统之外,例如手势追踪,包括视差效果和跟随手指的拖动;注释中明确说明属于关键承载逻辑的值也应排除。将这些内容列为有意保留的例外。
扩展现有实现。如果 app 已有 haptic wrapper,通常还带有 app 内开关,那么新的 haptic 角色必须通过该 wrapper 调用,绝不能绕过它,因为
sensoryFeedback
会绕过开关。如果现有 motion helper 已损坏或没有被使用,应明确说明,并提出修复或删除方案,绝不能在其旁边再创建第二个 helper。

3. Find the app's tone

3. 确定 app 的基调

Read the existing motion and the design (colours, type, how playful the copy is) and name the tone in a few words: calm and precise, soft and friendly, bold and playful. Every value you propose must fit it. A finance app should not get game springs.
阅读现有动效和设计,包括颜色、字体以及文案有多俏皮,并用几个词描述基调,例如平静而精准、柔和而友好、大胆而俏皮。你提出的每个值都必须符合这一基调。金融类 app 不应使用游戏式 spring。

4. Ground it in real apps

4. 以真实 app 为依据

Call
60fps_search_shots
once per job that needs a reference. Describe the motion and the tone, not the object: "sections fading in one after another as a screen loads, calm" finds arrivals, while "cards arriving" finds card stacks. Pick 3 to 5 shots that match the tone, judging by title, summary,
intensity
and
mood
, not by the score. Prefer shots whose
motion
tags suggest springs, fades or crossfades: timing driven by a
TimelineView
or custom math has no animation values to read. When nothing fits the tone (calm apps rarely find a calm celebration), the app's own best value is the source, and say so.
For those shots only, call
60fps_get_motion_code
. Use it only when
source
is
generated
, and read the
generated
field's own lines:
withAnimation
and
.animation
values, spring parameters, delays, haptic calls. Ignore the response's top-level
animation
,
recipes
and
stagger_delay
fields: they are generic starter values, often identical across shots. A
recipes
result has no shot numbers at all, and neither does a demo whose timing is computed, so skip it and pick another. The file is a self-running demo, so skip its loops and replays.
In this skill, do NOT show the code files to the user, even though the tool's own instructions say to: you only need a few timing lines from each, and the files are long. The numbers come from an AI recreation of a real app's video, so they are a strong reference, not a measurement. Each call counts against the user's daily allowance, so stop at 5.
每项需要参考的用途调用一次
60fps_search_shots
。描述动效和基调,而不是描述对象:“屏幕加载时各区块依次淡入,平静”会找到入场动效,而“卡片入场”会找到卡片堆叠效果。挑选 3 到 5 个符合基调的 shot,依据
title
、
summary
、
intensity
和
mood
进行判断,而不是依据分数。优先选择
motion
标签表明使用 spring、fade 或 crossfade 的 shot。由
TimelineView
驱动或使用自定义数学计算的 timing 没有可供读取的动画值。
如果没有内容符合基调,例如平静的 app 很少能找到平静的庆祝动效,就以 app 自身最好的值作为来源,并明确说明这一点。
仅针对这些 shot 调用
60fps_get_motion_code
。仅当
source
为
generated
时使用它,并读取
generated
字段中的具体行,包括
withAnimation
和
.animation
的值、spring 参数、delay 和 haptic 调用。忽略响应顶层的
animation
、
recipes
和
stagger_delay
字段,它们是通用初始值,而且不同 shot 中经常完全相同。返回
recipes
的结果没有任何 shot 编号,由计算得出 timing 的 demo 也没有可读数值,因此应跳过这类结果并另选一个。该文件是一个自动运行的 demo,因此跳过其中的 loop 和 replay。
在本 skill 中,绝不要向用户展示代码文件,即使工具自身的指示要求展示。你只需要从每个文件中获取几行 timing,而这些文件都很长。这些数字来自 AI 对真实 app 视频的复现,因此是可靠的参考依据,而不是精确测量结果。每次调用都会占用用户的每日额度,因此最多调用 5 次。

5. Propose the system

5. 提出系统方案

Keep it small. A system nobody can remember is not a system.
  • Animations: about 4 named values, one per job, for example
    Motion.feedback
    ,
    Motion.change
    ,
    Motion.arrival
    ,
    Motion.emphasis
    , plus the deliberate exceptions from step 2. Use the newest spring API the floor allows. At iOS 17 and up, prefer
    .spring(duration:bounce:)
    , or
    .smooth
    ,
    .snappy
    and
    .bouncy
    with
    duration:
    . Note that
    extraBounce:
    is added to the preset's own bounce (
    .snappy
    already has some), so write converted values with
    .spring(duration:bounce:)
    . To convert
    .spring(response:dampingFraction:)
    : duration equals the response; bounce is 1 minus the damping fraction when the damping fraction is 1 or less, and 1 divided by the damping fraction, minus 1, when it is above 1. Any
    blendDuration
    is dropped.
  • Stagger: one step per item for lists that reveal in order. Stagger only the first several items (about 8) and show the rest together, so long lists never make people wait and the steps stay big enough to read.
  • Haptics: a role each for selection, a light tap, a firm tap for primary or destructive actions, success and error, but only the roles the app has a call site for or the proposal adds one for. Success is for real achievements only, never routine refreshes or syncs. One haptic per event, never one per item in a list.
  • Reduce Motion: one rule for the whole app. In views read
    @Environment(\.accessibilityReduceMotion)
    ; in models and other code with no environment, read
    UIAccessibility.isReduceMotionEnabled
    on the main actor. Transitions become
    .opacity
    . Swapping the animation alone does not stop movement: a state-driven
    scaleEffect
    or
    offset
    still scales or moves, just faster, so hold those values at rest. Layout changes (rows inserted, a list growing) always move their neighbours; there, a short
    .easeInOut
    (about 0.2 s) is the practical rule. Meaningful changes still change, they just do not travel. A helper that returns
    nil
    gives an instant change, not a fade: use
    nil
    only where instant is the deliberate choice (a whole-screen move), and a short
    .easeInOut
    where the rule is a crossfade.
Present it as a table: name, value, what it is for, which current values it replaces, and where the value comes from (a shot, or the app's own code). Then list every call site that would change, grouped by file, with the old and new value, and every sleep that must change with it. Ask before touching them.
保持精简。没人能够记住的系统不是系统。
  • 动画:大约 4 个具名值,每项用途一个,例如
    Motion.feedback
    、
    Motion.change
    、
    Motion.arrival
    、
    Motion.emphasis
    ,再加上第 2 步中有意保留的例外。使用最低版本允许的最新 spring API。在 iOS 17 及更高版本中,优先使用
    .spring(duration:bounce:)
    ,或者带
    duration:
    的
    .smooth
    、
    .snappy
    和
    .bouncy
    。注意,
    extraBounce:
    会叠加在预设自身的 bounce 上,而
    .snappy
    已经包含一定的 bounce,因此转换后的值应使用
    .spring(duration:bounce:)
    编写。将
    .spring(response:dampingFraction:)
    转换为新 API 时,duration 等于 response;当 damping fraction 小于或等于 1 时,bounce 等于 1 减去 damping fraction;当 damping fraction 大于 1 时,bounce 等于 1 除以 damping fraction 再减去 1。所有
    blendDuration
    都会被丢弃。
  • Stagger:对于按顺序呈现的列表,每个项目使用一个步进。只对前几个项目使用 stagger,大约 8 个,其余项目一起显示,这样长列表不会让人等待,同时步进幅度也足够清晰易读。
  • 触觉反馈:分别为选择、轻触、主要或破坏性操作的强触感、成功和错误定义角色,但只保留 app 已有调用点的角色,或方案中明确新增的角色。成功反馈只能用于真正的成就,绝不能用于常规刷新或同步。每个事件只使用一次 haptic,列表中绝不能为每个项目分别触发。
  • Reduce Motion:整个 app 使用一条统一规则。在 views 中读取
    @Environment(\.accessibilityReduceMotion)
    ;在 models 和其他无法访问 environment 的代码中,在 main actor 上读取
    UIAccessibility.isReduceMotionEnabled
    。转场改为
    .opacity
    。只替换动画并不能停止运动:由状态驱动的
    scaleEffect
    或
    offset
    仍然会缩放或移动,只是速度更快,因此应让这些值保持静止状态。布局变化,例如插入行或列表增长,始终会移动相邻元素;在这种情况下,采用较短的
    .easeInOut
    ,约 0.2 s,是切实可行的规则。有意义的变化仍然会呈现,只是不再发生位移。返回
    nil
    的 helper 会产生即时变化,而不是淡入淡出。只有在即时变化是刻意选择时才使用
    nil
    ,例如整个屏幕的移动;当规则要求 crossfade 时,应使用较短的
    .easeInOut
    。
使用表格呈现方案,列出名称、值、用途、它会替换哪些当前值,以及该值的来源,即某个 shot 或 app 自身的代码。然后按文件分组列出所有需要修改的调用点,并注明旧值和新值,同时列出所有必须同步修改的 sleep。修改前先征求用户同意。

6. Write the file

6. 编写文件

When the user agrees, write the values into the app's existing motion file or type if it has one (from step 2), otherwise a new
Motion.swift
, with short comments saying what each is for, the haptic roles (through the app's haptics wrapper if it has one,
sensoryFeedback
if not), and small helpers that apply the Reduce Motion rule. Compile it before handing it over: build the app if you can, or typecheck the file against the floor from step 1, adding a small stub file for any app type it uses:
xcrun --sdk iphonesimulator swiftc -typecheck -target arm64-apple-ios<floor>-simulator \
  -swift-version <mode> Motion.swift Stub.swift
Add the project's isolation and concurrency flags if it sets them. The
-target
floor is what catches an API that is too new. Then migrate call sites in small batches, file by file, only if the user asks, moving each coupled sleep to the animation's completion.
Write plainly: short sentences, common words. Never use em dashes or en dashes.
用户同意后,将这些值写入 app 已有的 motion 文件或类型中,即第 2 步找到的文件;如果没有,则新建
Motion.swift
。添加简短注释,说明每个值的用途、haptic 角色,以及应用 Reduce Motion 规则的小型 helper。如果 app 已有 haptic wrapper,则通过该 wrapper 触发 haptic;如果没有,则使用
sensoryFeedback
。
交付前先编译。如果可以,构建整个 app;否则,根据第 1 步确定的最低版本对文件执行 typecheck,并为其中使用的任何 app 类型添加一个小型 stub 文件:
xcrun --sdk iphonesimulator swiftc -typecheck -target arm64-apple-ios<floor>-simulator \
  -swift-version <mode> Motion.swift Stub.swift
如果项目设置了 isolation 和 concurrency flags,也要添加这些参数。
-target
使用的最低版本能够捕获过新的 API。只有在用户提出要求后,才按文件分批迁移调用点,并将每个耦合的 sleep 移到动画完成时执行。
文字要平实:使用简短句子和常用词。不要使用 em dash 或 en dash。

Not for

不适用场景

Android, Flutter, React Native or web. A single animation: use the
motion-reference
skill. Where an app should animate at all: use
find-animation-opportunities
first.
Android、Flutter、React Native 或 web。单个动画:使用
motion-reference
skill。判断 app 是否应该在某个位置加入动画:先使用
find-animation-opportunities
。