360 Secure SkillHub
360 安全技能中心

motion-system

检查 iOS 工程部署版本与现有动画、触觉反馈及 Reduce Motion 用法,结合 60fps 动效参考提出少量命名动效值,生成 Motion.swift 并在用户同意后按需迁移调用点、编译校验。

60fps-Design
热度 0.00
最近更新 2026-10-18 16:01:49
分类:开发依赖:代码执行 / 读写文件 / MCP 服务应用场景:代码生成

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add 60fps-design/plugin/motion-system
Metadata
name
motion-system
description
Give an iOS app one consistent motion language: a small set of named timings, springs and haptics in a single Motion.swift, derived from the app's own code and from real shipped apps with the same feel. Use when asked to make an app's animations consistent, create motion tokens or a motion system, clean up scattered animation values, or define how the app should move. SwiftUI, iOS only.
license
MIT
compatibility
Needs the 60fps MCP server (https://mcp.60fps.design/mcp), signed in with OAuth.
metadata
{"author":"60fps-Design","version":"1.0"}

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.

1. Find the floor

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.

2. Inventory what the app does now

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.

3. Find the app's tone

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.

4. Ground it in real apps

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.

5. Propose the system

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.

6. Write the file

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.

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.