Generate premium SwiftUI animations in the legendary-Animo style — spring physics, CoreHaptics, glass morphism, SF Symbols 7 draw animations, complete compilable files.
Add this skill
npx mdskills install iAmVishal16/swiftui-microinteractionsComprehensive SwiftUI animation generator with physics presets, designer vocabulary mapping, and production-ready code output
1---2name: swiftui-microinteractions3description: Generate premium SwiftUI animations in the legendary-Animo style — spring physics, CoreHaptics, glass morphism, SF Symbols 7 draw animations, complete compilable files.4---56Generate a complete, compilable SwiftUI animation file in the legendary-Animo style. No placeholders. No TODO comments.78## When to Use9- Building a SwiftUI button, gesture, slider, or liquid effect with premium physics10- Need haptic feedback timed correctly to animation phases11- Creating a drag interaction with resistance, snap, or threshold trigger12- Animating SF Symbols with draw-on, breathe, bounce, replace, or variable color effects (iOS 17–26)13- Building a Canvas loader that traces a shape outline with a comet trail (infinity, star, polygon…)14- Editing an existing SwiftUI animation file1516## Mode Detection17- **Edit mode**: prompt contains "edit", "update", "add X to", "change", or a `.swift` filename → read file first, change only what's asked, overwrite file on disk18- **Create mode**: everything else → generate a new complete file and write it to disk1920---2122## Intent Inference (runs before code generation)2324Before generating any code, parse the prompt for under-specified details and resolve them to defaults. Print a `🔍 Inferred from prompt:` block so the user can see — and override — what was auto-resolved. Only print lines that were actually inferred (not ones the user explicitly stated).2526### Step 0 — Normalize designer vocabulary2728Translate any designer words in the prompt into skill-understood terms before running subsequent steps. This lets non-technical prompts ("snappy card that rips with anticipation") produce the same output as fully specified ones.2930**Animation feel words → physics preset:**3132| Designer says | Maps to |33|---|---|34| snappy / crisp / tight | UI pop `0.35/0.6` |35| bouncy / springy / elastic | Snap `0.35/0.5` |36| stretchy / rubber / wobbly | 3-phase rubber-band (see iOS 26 section) |37| melts / floats / soft / gentle | Slow morph `0.6/0.8` |38| stiff / precise / mechanical | Precision stiff `interpolatingSpring(220, 22)` |39| linear / steady / constant | `.linear(duration:)` |4041**Motion principle words → archetype / animation signals:**4243| Designer says | Implies |44|---|---|45| anticipation | pre-stretch phase before main move |46| follow-through | add overshoot to settle animation |47| squash & stretch | scale deformation on impact (x expand, y compress) |48| staging / stagger | offset `.animation(delay:)` per element |49| pulse / breathe | `.breathe` isActive loop |50| ripple | radial spread `Circle` scale from tap point |51| flood | Liquid Toggle archetype, metaball fill |52| wiggle / shake | `.wiggle value:` — error / rejection signal |53| pop | quick `.scaleEffect` overshoot + snap back |54| morph | Glass Morph archetype or shape interpolation |5556**UI element names → SwiftUI primitives:**5758| Designer says | Also called | SwiftUI |59|---|---|---|60| bottom sheet / tray / drawer / panel | pull-up, modal sheet | `.sheet` + `.presentationDetents` |61| modal / dialog / overlay / lightbox | full-screen sheet | `.sheet` (full) or `.fullScreenCover` |62| popover / tooltip / callout / bubble | hover card | `.popover` |63| toast / snackbar / nudge / banner | inline notification | custom overlay + auto-dismiss `.task` |64| alert / warning / confirm | dialog | `.alert` or `.confirmationDialog` |65| card / tile / surface | panel | `RoundedRectangle` + material |66| chip / tag / pill / badge | filter tag | custom `Capsule` |67| FAB / floating action button | floating button | absolute-positioned `Button` in `ZStack` |68| segmented control / pill selector / tab strip | toggle group | `Picker(.segmented)` or custom pill |69| skeleton / shimmer / placeholder | loading placeholder | `redacted(.placeholder)` or animated opacity |70| spinner / loader / throbber | activity indicator | `ProgressView` |71| contextual menu / long-press menu | press-hold menu | `.contextMenu` |72| action sheet / bottom action menu | options sheet | `.confirmationDialog` |7374### Step 1 — Symbol → Effect lookup7576If the prompt names a symbol (or describes one by subject) and does **not** specify an animation effect, apply this table:7778| Symbol / subject | Type | Auto-effect |79|---|---|---|80| `signature`, `pencil`, `pencil.line`, `scribble`, `lasso` | stroke | `.drawOn` auto-loop |81| `checkmark`, `heart` (outline), `star` (outline), `wifi`, `antenna.radiowaves` | stroke | `.drawOn` auto-loop |82| `heart.fill`, `star.fill`, `circle.fill` | flood-fill | `.bounce value:` (drawOn invisible on fill shapes) |83| `circle.dotted`, `rays`, `waveform` | animated structure | `.variableColor.iterative.reversing` or `.breathe` |84| `arrow.clockwise`, `arrow.2.circlepath`, `gear` | rotation | `.rotate` (indefinite, `isActive`) |85| `bell`, `bell.fill` | discrete action | `.bounce value:` |8687### Step 2 — Showcase mode vs. interaction mode8889- **No user action described** (no "tap to", "on submit", "when user"…) → **showcase mode**: drive with `.task` auto-loop. Symbol animates on its own without any tap required.90- **User action described** → gate the animation behind that action only. Do not add an auto-loop.9192### Step 3 — Apply Defaults Contract9394Resolve any unspecified UI elements using the Defaults Contract (see below).9596### Step 4 — Print inference log9798```99🔍 Inferred from prompt:100 Vocab → "<designer word>" → <normalized term> (only if Step 0 fired)101 Symbol → <name> (<stroke|fill>) → <effect>102 Mode → showcase (auto-loop) | interaction (tap/submit)103 Container → .sheet (Apple default) | inline | none104 Controls → Clear · Done | none105```106107---108109## Defaults Contract110111**Absent = Apple HIG default.** Any UI element not mentioned in the prompt defaults to the Apple system primitive — never invent a custom modal, overlay, or container when a system one fits.112113| Element | Apple default | When to escalate |114|---|---|---|115| Container | `.sheet(isPresented:)` | Prompt names custom modal / full-screen |116| Dismiss | `Button("Done")` in `.toolbar(.confirmationAction)` | Prompt names custom button |117| Clear / reset | `Button("Clear")` in `.toolbar(.cancellationAction)` | Drawing/signature context only |118| Background inside sheet | System sheet bg (no custom dark fill) | Prompt explicitly names dark/custom bg |119| Navigation | `NavigationStack` | Multi-step flow or title implied |120| Loading state | `.progressView()` or symbol `.variableColor` | Prompt names a specific loader shape |121122**Drawing / signature context rule** — if the prompt contains "signature", "draw", "sketch", or "handwrite", automatically apply:123- Container: `.sheet(isPresented:)`124- Toolbar: `Button("Clear")` (`.cancellationAction`) + `Button("Done")` (`.confirmationAction`)125- No custom dark background inside the sheet (let the system sheet surface show through)126127---128129## Asset Scanning (run before generating any image-dependent animation)130131If the animation involves images, photos, or cards, scan for existing assets first:1321331. Check these paths in order:134 - `legendary-Animo/Res/Assets.xcassets/Images/`135 - `Assets.xcassets/Images/`136 - `Assets.xcassets/`1371382. List `.imageset` folder names — strip `.imageset` suffix to get the `Image("name")` string1391403. **If found** → use them directly. Print:141 ```142 🖼️ Assets found: photo1, photo2 … (using in file)143 ```1441454. **If not found** → use `Image(systemName: "photo.fill")` in a colored `RoundedRectangle`. Include Asset Notes at the end.146147Never invent asset names. Only reference names confirmed to exist on disk.148149---150151## Haptics — check project first152153Before including haptic code, check if `HapticFeedback.swift` exists anywhere in the project:154- **If found** → call `HapticFeedback.lightImpact()` etc. directly. Do NOT re-declare the struct.155- **If not found** → call `UIImpactFeedbackGenerator` / `UISelectionFeedbackGenerator` directly inline, and add `import UIKit` at the top.156157### When to add haptics vs. skip158159**Add haptics when:**160- A gesture has a threshold that triggers an irreversible action (drag-to-delete, swipe-to-dismiss) → `heavyImpact` at the threshold crossing161- A toggle switches between two meaningful states → `mediumImpact` on commit162- A destructive action (delete, clear, reset) is confirmed → `heavyImpact`163- A scrub or dial moves across discrete data points → `selectionChanged` per step164- A glass morph or metaball fuse/separate completes → `mediumImpact`165166**Skip haptics when:**167- Pure symbol showcase with auto-loop and no user interaction168- Loading indicator or ambient background animation (stars, particles drifting)169- Prompt describes no user action at all170171**Haptic ladder for gesture arcs:**1721. `lightImpact` — drag starts / touch down1732. `mediumImpact` — threshold crossed / halfway1743. `heavyImpact` — action commits / destruction confirmed1754. `selectionChanged` — discrete value scrub / step176177**SourceKit `HapticFeedback` false positive — always ignore:**178In files written to `Carousels/` or `Animations/`, SourceKit reports `Cannot find 'HapticFeedback' in scope`. This is **not a real error** — SourceKit analyzes the new file in isolation and doesn't see other module members. `HapticFeedback.swift` is registered in the Sources build phase; the real compiler resolves it correctly. Do not add `import UIKit`, do not re-declare the struct, do not alter the code.179180---181182## Style Rules183184**Backgrounds:** `Color(white: 0.06).ignoresSafeArea()` standalone · `Color("BgColor").ignoresSafeArea()` in-project185186**Glass surfaces (pre-iOS 26):** `.ultraThinMaterial` or `.thickMaterial` clipped to shape · track background `.white.opacity(0.06)` + 1pt stroke `.white.opacity(0.08)`187188**Opacity levels:** ghost `0.06–0.08` · subtle `0.12` · inactive `0.3` · secondary `0.5` · active `0.8–0.9` · full `1.0`189190**Colors:** white + opacity dominant · cyan `Color(red: 0.45, green: 0.65, blue: 1.0)` · green `Color(red: 0.55, green: 0.95, blue: 0.75)` · never `.blue/.green/.red` on dark bg191192**Gradients:** two-tone only · blob fill `[.white, Color(white: 0.88)]` · progress `[cyan, green]`193194---195196## Light Theme197198The Style Rules above are dark-first. For a **light** UI (dashboards, cards, sheets on white), depth comes from **tonal deltas, not shadows** — premium light design avoids drop shadows between stacked surfaces.199200**Tonal stack — separate surfaces by lightness:**201202| Role | Value |203|---|---|204| Screen background | `Color(white: 0.92)` (or gradient `0.93 → 0.87`) |205| Card / row surface | `Color.white` |206| Inset well (icon circle, field) | `Color(white: 0.95)` |207| Track / divider | `Color(white: 0.88)` |208| Ink (primary text) | `Color(white: 0.10)` |209| Muted (secondary text) | `Color(white: 0.55)` |210211Rules:212- **No `.shadow` between stacked light surfaces** — a white row on a `0.92` background already reads as raised. Reserve a shadow for a single floating primary (e.g. a dark CTA), never for every row.213- Accent colors must be **deepened** for contrast on white — e.g. cyan `Color(red: 0.30, green: 0.52, blue: 0.95)`, the opposite of the dark-bg cyan.214- A dark capsule CTA (`Color(white: 0.10)` fill, white label) is the light-theme equivalent of the glass button.215216### Adaptive (support BOTH light + dark)217218When a screen must follow the system appearance, never hardcode `.white`/`.black` or force `.preferredColorScheme`. Drive everything off `@Environment(\.colorScheme)` + semantic colors:219220```swift221@Environment(\.colorScheme) private var scheme222private var isDark: Bool { scheme == .dark }223```224- **Text** → `.primary` / `.secondary` (auto-invert). Replace every `.white.opacity(x)` with `.primary.opacity(x)` / `.secondary`.225- **Glass card — the official way (iOS 26):** use the real Liquid Glass API and **tint it dark** rather than faking glass with material — `.glassEffect(.regular.tint((isDark ? .black : .white).opacity(0.28)), in: shape)` keeps genuine specular + refraction while reading as a dark (or light) notification surface. Gate with `if #available(iOS 26.0, *)`; **fall back** to `.ultraThinMaterial` + a scheme-aware tint overlay (`isDark ? .black.opacity(0.45) : .white.opacity(0.35)`) on iOS 18–25. (A *plain untinted* `glassEffect` reads light — always tint for a dark surface; don't reach for material on iOS 26.)226- **Gradients / strokes / shadows** → branch on `isDark` (e.g. navy bg in dark, blue-white in light; shadow `0.35` dark vs `0.12` light).227- **Accent gradients** → lighten in dark, deepen in light, so the accent stays visible on both backgrounds.228- A stored `let` can't read the environment — make scheme-dependent values **computed `var`s**.229230---231232## iOS 26 Liquid Glass233234Use `.glassEffect()` on iOS 26+, fall back to `.ultraThinMaterial` on older OS. Always wrap in a `@ViewBuilder` helper so both paths share the same call site:235236```swift237@ViewBuilder238private func glassShape(radius: CGFloat) -> some View {239 if #available(iOS 26.0, *) {240 RoundedRectangle(cornerRadius: radius, style: .continuous)241 .fill(.clear)242 .glassEffect(in: RoundedRectangle(cornerRadius: radius, style: .continuous))243 } else {244 RoundedRectangle(cornerRadius: radius, style: .continuous)245 .fill(.ultraThinMaterial)246 .overlay(247 RoundedRectangle(cornerRadius: radius, style: .continuous)248 .strokeBorder(Color.white.opacity(0.12), lineWidth: 1)249 )250 }251}252```253254Apply to any shape — pill, capsule, circle. Never hardcode `.ultraThinMaterial` for new components when iOS 26 is a target.255256### Tinted glass (colored states)257258For destructive / accent buttons, use the built-in tint API — never layer a colored shape on top of glass:259260```swift261.glassEffect(.regular.tint(dangerColor), in: Circle())262```263264Danger red: `Color(red: 0.87, green: 0.32, blue: 0.42)` · Accent blue: `Color(red: 0.45, green: 0.65, blue: 1.0)`.265266### `GlassEffectContainer` — morphing clusters267268When multiple glass elements should fuse and separate with the iOS 26 metaball effect (e.g. a capsule that morphs into a separate circle), wrap them in a `GlassEffectContainer`:269270```swift271@Namespace private var glassNS272273GlassEffectContainer(spacing: 18) {274 HStack(spacing: 28) { // see spacing trap below275 capsule276 .glassEffect(in: Capsule())277 .glassEffectID("capsule", in: glassNS)278279 if isExpanded {280 circle281 .glassEffect(.regular.tint(.red), in: Circle())282 .glassEffectID("circle", in: glassNS)283 }284 }285}286```287288**Fusion-threshold spacing trap** — `GlassEffectContainer(spacing:)` is the fusion threshold, not visual padding. Any two glass elements within that distance will visually blob together permanently, even at rest. Always make the layout spacing **greater than** `containerSpacing`:289290| containerSpacing | HStack spacing | Result |291|---|---|---|292| 18 | 10 | ❌ Permanent fusion — edges distorted at rest |293| 18 | 28 | ✅ Clean shapes at rest, metaball morph only during transition |294295Rule: `layoutSpacing > containerSpacing + 6pt` for safe margin.296297### Edge distortion trap298299Never apply `.scaleEffect(x:y:anchor:)` with offset anchors (`.leading` / `.trailing`) to **individual** glass elements — subpixel rendering at the anchor edge causes visible edge distortion even when scale = 1.0. Apply container-wide rubber stretch instead:300301```swift302GlassEffectContainer(spacing: 18) { ... }303 .scaleEffect(x: 1.0 + stretch * 0.05, y: 1.0 - stretch * 0.025, anchor: .center)304```305306Also: don't add redundant `.clipShape(Capsule())` on top of `.glassEffect(in: Capsule())` — `glassEffect` already clips.307308### 3-phase rubber-band toggle309310For glass elements that pop apart or fuse on tap, stack 3 animations to feel like real rubber:311312```swift313private func toggle() {314 HapticFeedback.mediumImpact()315316 // Phase 1 — pre-stretch (rubber tensions)317 withAnimation(.spring(response: 0.28, dampingFraction: 0.55)) {318 stretchAmount = 1.0319 }320 // Phase 2 — morph fires at peak tension321 DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {322 withAnimation(.spring(response: 0.55, dampingFraction: 0.72)) {323 isExpanded.toggle()324 }325 }326 // Phase 3 — release snap-back with overshoot327 DispatchQueue.main.asyncAfter(deadline: .now() + 0.28) {328 withAnimation(.spring(response: 0.45, dampingFraction: 0.55)) {329 stretchAmount = 0330 }331 }332}333```334335### Light backgrounds for glass demos336337To showcase iOS 26 glass effects, use a light gradient background — dark backgrounds hide the refraction. Standalone demo:338339```swift340LinearGradient(colors: [Color(white: 0.97), Color(white: 0.88)],341 startPoint: .topLeading, endPoint: .bottomTrailing)342 .ignoresSafeArea()343```344345---346347## SF Symbols Animation (iOS 17–26)348349> **Auto-effect selection:** See Intent Inference § Symbol → Effect lookup above. The table there maps symbol names to the correct effect automatically — no need to specify `.drawOn`, `.breathe`, etc. in the prompt unless you want to override the default.350351### `.drawOn` — iOS 26 only (SF Symbols 7)352353**`isActive` is inverted.** This is the single most common mistake:354355| isActive | Visual state | What plays |356|---|---|---|357| `true` | Hidden (pre-draw) | Nothing on initial render |358| `false` | Visible | drawOn traces stroke on |359| `false → true` | Hidden | Reverse (draw-off) plays automatically |360361**Correct pattern — always name the state variable to reflect "hidden":**362363```swift364@State private var isDrawSymbolHidden = true // starts hidden365366Image(systemName: "checkmark")367 .symbolEffect(.drawOn, options: .speed(0.5), isActive: isDrawSymbolHidden)368 .task {369 try? await Task.sleep(for: .seconds(0.5))370 while !Task.isCancelled {371 isDrawSymbolHidden.toggle() // false → draws on; true → draws off372 try? await Task.sleep(for: .seconds(2.0))373 }374 }375```376377**Symbol choice matters for `.drawOn`:**378- `heart.fill`, `star.fill` — flood-fill shapes, no stroke path. drawOn appears to jump from hidden to filled instantly. Not suitable for demonstrating the pen-trace effect.379- `checkmark`, `heart` (outline), `signature`, `wifi`, `star` (outline) — stroke-based, clearly show the trace.380381Do NOT stack `.drawOn` and `.drawOff` on the same image. They conflict and keep the symbol invisible.382383**`.drawOn` only plays on a *transition* — never on appear.** `isActive:` traces the path when the bound value *changes* while the view is visible. It does NOT replay when the view appears already in the visible state. Two consequences:384385- **"Draw a … demo" / "animate …" → auto-play by default.** When the prompt asks the symbol to draw/animate itself (a showcase, a hint, a loading state), drive it with the `.task` loop above so it traces on its own. Only gate it behind a tap/state change when the prompt explicitly describes a user action ("tap to sign", "on submit"). Reaching for the interaction trigger when the user wanted a self-playing demo is the most common way a drawOn build looks "broken" — the API is present and correct, but nothing ever fires it.386- **If a tap drives it, the pad/area is empty until the first tap.** That's correct behavior, not a bug — but confirm it's what the prompt wanted.387388**Verification trap — never validate a transition by pre-setting state to its destination.** Setting `@State private var isDrawSymbolHidden = false` at init renders the symbol *already fully drawn, with no transition* — so a screenshot shows a complete symbol and falsely confirms "it works." That validates layout, not motion. To actually verify the trace: launch with the auto-loop running (or script the tap), and capture a **mid-trace frame** (a partial stroke). A fully-drawn symbol in a screenshot proves nothing about whether the draw animated. This applies to every `isActive:`/`value:` symbolEffect, not just drawOn.389390---391392### Other Symbol Effects (iOS 17+)393394```swift395// Discrete — fires once per value change (user actions, additions)396.symbolEffect(.bounce, value: count)397.symbolEffect(.wiggle, value: errorTrigger)398399// Indefinite — runs while active (states: recording, loading, scanning)400.symbolEffect(.breathe, isActive: isRecording)401.symbolEffect(.variableColor.iterative.reversing, isActive: isLoading)402.symbolEffect(.rotate, isActive: isSyncing)403404// Content transition — swapping symbols on state toggle (always needs withAnimation)405Image(systemName: isPlaying ? "pause.fill" : "play.fill")406 .contentTransition(.symbolEffect(.replace))407// trigger with: withAnimation(.smooth) { isPlaying.toggle() }408```409410**Loop driver — use `.task`, not `Timer`:**411```swift412.task {413 while !Task.isCancelled {414 try? await Task.sleep(for: .seconds(interval))415 count += 1 // or toggle state416 }417}418```419`.task` cancels automatically when the view disappears. Timer leaks.420421---422423## Spring Presets424425| Feel | Value |426|---|---|427| Snap / bounce | `.spring(response: 0.35, dampingFraction: 0.5)` |428| UI pop | `.spring(response: 0.35, dampingFraction: 0.6)` |429| Standard settle | `.spring(response: 0.4, dampingFraction: 0.65)` |430| Physics settle | `.spring(response: 0.45, dampingFraction: 0.7)` |431| Slow morph | `.spring(response: 0.6, dampingFraction: 0.8)` |432| Precision stiff | `.interpolatingSpring(stiffness: 220, damping: 22)` |433| Dial / scrub | `.interactiveSpring(response: 0.3, dampingFraction: 0.7)` |434435Never use bare `.spring()` — always explicit `response` + `dampingFraction`. If user supplies values, use them verbatim. Map feel words: "stretchy" → 0.4/0.5, "snappy" → 0.35/0.6, "melts" → 0.6/0.8.436437**Archetype → physics default** (use when the prompt doesn't name a feel):438439| Archetype | Default physics |440|---|---|441| Symbol Showcase | `.easeInOut(duration: 0.4)` — symbol effects own their timing |442| Liquid Toggle | UI pop `0.35/0.6` |443| Gesture Card | Physics settle `0.45/0.7` |444| Sheet Interaction | Standard settle `0.4/0.65` |445| Data Dashboard | Dial/scrub `.interactiveSpring(response: 0.3, dampingFraction: 0.7)` |446| Glass Morph | 3-phase rubber-band (see iOS 26 section) |447| Particle / Physics Sim | No SwiftUI spring — custom physics loop |448| Loading Indicator | `.linear(duration: 0.8)` — never spring (loops look jittery) |449450---451452## Press Feedback — `PressableScale`453454Every tappable row/button should shrink under the finger and feel like a real press. Use a **gesture-driven** modifier, not `Button`/`ButtonStyle` — `ButtonStyle.isPressed` cannot fire a haptic on the press-*down* edge, but a `DragGesture(minimumDistance: 0)` can.455456```swift457private struct PressableScale: ViewModifier {458 var pressedScale: CGFloat = 0.96459 let action: () -> Void460 @State private var isPressed = false461462 func body(content: Content) -> some View {463 content464 .scaleEffect(isPressed ? pressedScale : 1.0)465 .animation(.spring(response: 0.3, dampingFraction: 0.6), value: isPressed)466 .contentShape(Rectangle())467 .simultaneousGesture( // simultaneous → won't block parent scroll468 DragGesture(minimumDistance: 0)469 .onChanged { _ in470 if !isPressed { // haptic on the DOWN edge only471 isPressed = true472 HapticFeedback.lightImpact()473 }474 }475 .onEnded { v in476 isPressed = false477 // lift-inside-to-fire: dragging off cancels, like a real UIButton478 if abs(v.translation.width) < 20, abs(v.translation.height) < 20 { action() }479 }480 )481 }482}483extension View {484 func pressable(scale: CGFloat = 0.96, action: @escaping () -> Void = {}) -> some View {485 modifier(PressableScale(pressedScale: scale, action: action))486 }487}488```489490Non-obvious rules baked in:491- **`.simultaneousGesture`** (not `.gesture`) so a row inside a `ScrollView`/`List` still scrolls.492- **Haptic on the down edge only** (`if !isPressed`) — not on every `onChanged` tick.493- **Lift-inside-to-fire** — verify the finger lifted within ~20pt before running `action`; a drag-away must cancel.494- `pressedScale` ≈ `0.90` for round icon buttons, ≈ `0.965` for wide rows.495496---497498## Entrance / Appear Animation499500Premium screens *assemble themselves*. Stagger children in on appear with a single `@State` flag.501502```swift503@State private var appeared = false504505ForEach(Array(items.enumerated()), id: \.element.id) { i, item in506 rowView(item)507 .opacity(appeared ? 1 : 0)508 .offset(y: appeared ? 0 : 16)509 .scaleEffect(appeared ? 1 : 0.97, anchor: .top)510 .animation(.spring(response: 0.5, dampingFraction: 0.82)511 .delay(0.10 + Double(i) * 0.07), value: appeared)512}513.onAppear { appeared = true } // trigger once — NEVER init appeared = true514```515516- Combine `opacity + offset(y:) + scaleEffect(anchor:.top)` for a soft rise-and-settle; `0.07s` per-index delay reads as a cascade.517- **Inside a `.sheet`, `@State` resets on every presentation** — so this re-fires each time the sheet opens, a free "assembles itself" entrance with no extra code.518- Verification trap (same as `.drawOn`): never initialise the flag to its destination — the transition won't play.519520---521522## Custom Transitions — rubber-band entrance523524`.transition(.scale)` only interpolates from its scale **to 1.0** — so it can't make an element enter *oversized* and settle back. For an "enters wider, rubber-bands to true size" pop, write a custom modifier transition and drive it with a **low-damping (bouncy) spring** so it overshoots:525526```swift527private struct WidthPopModifier: ViewModifier {528 let active: Bool529 func body(content: Content) -> some View {530 content531 .scaleEffect(x: active ? 1.16 : 1.0, y: active ? 1.07 : 1.0, anchor: .top) // anisotropic → wider532 .offset(y: active ? -34 : 0)533 .opacity(active ? 0 : 1)534 }535}536private extension AnyTransition {537 static var widthPop: AnyTransition {538 .modifier(active: WidthPopModifier(active: true), identity: WidthPopModifier(active: false))539 }540}541542// usage — the surrounding withAnimation's spring governs the transition:543withAnimation(.spring(response: 0.5, dampingFraction: 0.55)) { items.insert(item, at: 0) }544// on the view: .transition(.asymmetric(insertion: .widthPop, removal: .move(edge: .top).combined(with: .opacity)))545```546547- **Anisotropic scale** (`x > y`) makes it read as a *width* stretch, not a uniform zoom.548- The rubber-band comes from the **spring damping**, not the transition: `dampingFraction ≈ 0.55` overshoots; `≈ 0.8` settles flat. Lower = bouncier.549- `.modifier(active:identity:)` is the general recipe for any entrance the built-in transitions can't express.550551---552553## Data Dashboard Patterns554555- **Animated proportional bar** — capsule track + fill whose width grows from 0 on appear and re-springs on data change:556```swift557GeometryReader { geo in558 ZStack(alignment: .leading) {559 Capsule().fill(track).frame(height: 6)560 Capsule().fill(ink.opacity(0.85))561 .frame(width: geo.size.width * (appeared ? share : 0), height: 6)562 .animation(.spring(response: 0.6, dampingFraction: 0.85), value: appeared)563 .animation(.spring(response: 0.5, dampingFraction: 0.8), value: selection)564 }565}.frame(height: 6)566```567- **`.numericText` as a count-up** — not just for currency. A hero metric that changes with a segmented selector rolls its digits via `.contentTransition(.numericText(value: Double(total)))` + `.animation(..., value: period)`. Pair the segment switch with `selectionChanged` haptic and a sliding indicator (see Tab Bar Patterns).568569---570571## State & Code Rules572573- Default: pure `@State` for all gesture tracking · `@GestureState` only if value must auto-reset574- Constants: camelCase `private let` at struct level (2–5 values) — never SCREAMING_SNAKE_CASE575- Liquid metaball: render blobs in a `Canvas` with `.addFilter(.alphaThreshold)` + `.addFilter(.blur)` — see the **Canvas Metaball** section. (Older alt: white circles on black + `.blur` + `.contrast(50)` + `.blendMode(.screen)`.)576- Multi-phase sequences: stacked `DispatchQueue.main.asyncAfter` with overlapping delays577578**ZStack frame trap — always apply both rules together:**579When a ZStack has a fixed `.frame(height:)` AND contains a `LazyVGrid`, `List`, or any tall component:5801. Conditionally render the tall child only when needed (`if isExpanded || childVisible`)5812. Add `.clipped()` to the ZStack582583Relying on `.opacity(0)` alone does NOT prevent overflow — the component still renders outside the frame and changing `height` constants has no visual effect.584585**Floating bar layout:** Build multi-component bars (pill + separate action button) as `HStack` of independent views, not one monolithic ZStack. Each component gets its own glass background and shadow.586587**Sheet wrapping — outer launcher + private inner content:**588When a demo needs a native `.sheet`, use two structs in the same file. `private` model types defined in the file are visible to both structs — no access-level changes needed.589590```swift591struct MyDemoView: View { // public — registered in ContentView592 @State private var showSheet = false593 var body: some View {594 ZStack { Color("BgColor").ignoresSafeArea(); triggerButton }595 .sheet(isPresented: $showSheet) {596 MySheetContent()597 .presentationDetents([.height(540)])598 .presentationCornerRadius(28)599 .presentationDragIndicator(.visible)600 }601 }602}603604private struct MySheetContent: View { // private — only used as sheet body605 @Environment(\.dismiss) private var dismiss606 // sheet body here — X button calls dismiss()607}608```609610**Numeric text transition — split the currency prefix:**611`.contentTransition(.numericText(value:))` on a currency string like `"$173.11"` can glitch the `$` during the digit roll. Split prefix and number into separate `Text` views — only the numeric part gets the transition:612613```swift614HStack(alignment: .firstTextBaseline, spacing: 1) {615 Text("$") // static — no transition616 .font(.system(size: 32, weight: .bold))617 Text(String(format: "%.2f", amount))618 .font(.system(size: 44, weight: .bold))619 .contentTransition(.numericText(value: amount))620 .animation(.spring(response: 0.4, dampingFraction: 0.65), value: triggerValue)621}622```623624When the transition is driven by a discrete step (card carousel, dial tick): use `selectionChanged` haptic, not `mediumImpact` — it matches the numeric ticker feel.625626**File layout (mandatory MARK order):**627```628// MARK: - Model629// MARK: - Main View (tokens → @State → body → subviews → gesture → actions)630// MARK: - Supporting Shapes631// MARK: - Preview632```633634---635636## Archetype Catalog637638The archetype drives physics, haptics, and container defaults. Pick the closest match from the prompt — if ambiguous, choose the simpler one.639640| Archetype | Prompt signals | Container | Physics | Haptics |641|---|---|---|---|---|642| **Symbol Showcase** | "animate", "draw", "show" + symbol name | none (inline) | `.easeInOut` | none |643| **Liquid Toggle** | "toggle", "switch", "flood", "metaball" | inline | UI pop `0.35/0.6` | mediumImpact on toggle |644| **Gesture Card** | "drag", "pull", "rip", "swipe", "dismiss" | inline ZStack | Physics settle `0.45/0.7` | light start → heavy commit |645| **Sheet Interaction** | "sheet", "sign", "draw", "pick", "form" | `.sheet` | Standard settle `0.4/0.65` | selectionChanged on controls |646| **Data Dashboard** | "chart", "graph", "metric", "scrub", "data" | full-screen | Dial/scrub `.interactiveSpring` | selectionChanged per step |647| **Glass Morph** | "glass", "fuse", "morph", "capsule" | inline | 3-phase rubber-band | mediumImpact on morph |648| **Particle / Physics Sim** | "gravity", "fluid", "rope", "cloth", "sand" | full-screen | custom physics loop | lightImpact on touch |649| **Loading Indicator** | "loading", "spinner", "progress", "scanning" | inline | `.linear(duration:)` | none |650651Print the resolved archetype on the `🎯 Archetype:` line. If the user's prompt overrides any default in this table, use their value and note the override in parentheses.652653---654655## Tab Bar Patterns656657**Sliding selection indicator** — use `GeometryReader` + `offset(x:)`, never manual position math:658659```swift660GeometryReader { geo in661 let tabW = geo.size.width / CGFloat(tabCount)662 RoundedRectangle(cornerRadius: indicatorRadius, style: .continuous)663 .fill(Color.white.opacity(0.14))664 .padding(5)665 .frame(width: tabW)666 .offset(x: tabW * CGFloat(selectedIndex))667 .animation(.spring(response: 0.35, dampingFraction: 0.65), value: selectedIndex)668}669```670671**Active indicator spec** (matches Apple Music / iOS 26 HIG):672- Fill: `Color.white.opacity(0.14)` — solid presence, clearly distinct from glass background673- Padding: `5pt` inset on all sides (fills full tab height minus 5pt each edge)674- Corner radius: `outerRadius - 5` (softer than the outer pill, independent value ~16–18pt)675- Width: exactly `pillWidth / tabCount` — crisp slot division, no guessing676677**Tab cell layout** (icon + label, per HIG):678- Icon: 22pt, `.semibold` when active · `.regular` inactive679- Label: 10–11pt, `.semibold` when active · `.regular` inactive680- Both tinted with `activeColor` when selected · `white.opacity(0.5)` inactive681- `VStack(spacing: 4)` inside `.frame(maxWidth: .infinity).frame(height: barHeight)`682683**SF Symbol replace transition** — for any button that toggles state and changes its icon (e.g. more → close, play → pause, add → done):684685```swift686Image(systemName: isExpanded ? "xmark" : "ellipsis")687 .contentTransition(.symbolEffect(.replace.downUp))688 .animation(.spring(response: 0.35, dampingFraction: 0.6), value: isExpanded)689```690691- `.replace.downUp` — old icon scales down, new scales up. Use for open/close toggles.692- `.replace.upUp` — both scale up. Use for sequential forward actions.693- The button action must toggle: `isExpanded ? collapse() : expand()` — never hide the button to show a separate close button.694- Available iOS 17+. No fallback needed for legendary-Animo (iOS 16+ minimum → gate with `if #available(iOS 17, *)` only if targeting iOS 16).695696**Standard heights:** `barHeight = 49pt` (icon-only) · `barHeight = 68pt` (icon + label)697698---699700## Carousels & Paging701702**Velocity-aware paging — threshold on `predictedEndTranslation`, not raw translation.** A slow short drag shouldn't page; a fast flick should — even if the finger barely moved. The predicted end is velocity-aware:703704```swift705DragGesture(minimumDistance: 12)706 .onChanged { v in dragOffset = v.translation.width * 0.5 } // 0.5 = drag resistance707 .onEnded { v in708 let threshold: CGFloat = 52709 withAnimation(.spring(response: 0.42, dampingFraction: 0.72)) {710 if v.predictedEndTranslation.width < -threshold, index < count - 1 {711 index += 1; HapticFeedback.selectionChanged()712 } else if v.predictedEndTranslation.width > threshold, index > 0 {713 index -= 1; HapticFeedback.selectionChanged()714 }715 dragOffset = 0716 }717 }718```719720**Fan / card-stack layout** — position each card from its distance to the selected index, scale down siblings, z-order by proximity:721722```swift723let diff = CGFloat(index - selectedIndex)724card725 .scaleEffect(index == selectedIndex ? 1.0 : 0.86)726 .offset(x: diff * xStep + dragOffset, y: index == selectedIndex ? -10 : 14)727 .zIndex(index == selectedIndex ? 10 : Double(5 - abs(index - selectedIndex)))728 .animation(.spring(response: 0.42, dampingFraction: 0.72), value: selectedIndex)729```730731- `selectionChanged` (not `mediumImpact`) per card switch — discrete scrub feel.732- Apply a **resistance multiplier** (`* 0.5`) to `dragOffset` so the stack feels weighty under the finger.733- `MeshGradient` (iOS 18+, 9-point) makes a premium card/orb fill — animate the control points for a living surface.734735---736737## Stacked Cards (notification stack)738739Apple's collapsed notification stack: newest card full-size in front, older ones **peek behind** (scaled down + offset + dimmed by depth), tap to **expand** into a list, **swipe the front card to dismiss**, and **cap** the count so the oldest drops off the back.740741```swift742// front = index 0. Transform each card from its depth + an `expanded` flag:743let yOffset = expanded ? CGFloat(depth) * (cardHeight + 10) : CGFloat(depth) * 16 // peek step744let scale = expanded ? 1.0 : max(0.88, 1.0 - CGFloat(depth) * 0.06)745let opacity = expanded ? 1.0 : (depth < 3 ? 1.0 - Double(depth) * 0.22 : 0.0) // only ~3 peek746card.scaleEffect(scale).opacity(opacity).offset(y: yOffset).zIndex(Double(count - depth))747```748749- **Insert front + cap:** `withAnimation(.spring) { items.insert(new, at: 0); if items.count > cap { items.removeLast() } }` — newest pops in (pair with `widthPop`), oldest fades off the back.750- **Expand toggle:** `onTapGesture` on the stack flips `expanded` with `selectionChanged` haptic + `.spring(0.5/0.8)`.751- **Swipe-to-dismiss (front only):** gate the `DragGesture` to `depth == 0`; rubber-band `dragOffset`, `lightImpact` on start, past ~120pt `removeFirst()` + `heavyImpact`, else spring back.752- Each card `.transition(.asymmetric(insertion: .widthPop, removal: .move(edge: .top).combined(with: .opacity)))`.753- **"Notification" card surface:** on iOS 26 use authentic Liquid Glass — `.glassEffect(.regular.tint((isDark ? .black : .white).opacity(0.28)), in: shape)`; fall back to `.ultraThinMaterial` + scheme-aware tint below (see Light Theme → Adaptive). 28pt continuous radius + soft shadow.754755---756757## Canvas Metaball (goo / liquid extrusion)758759For elements that should **fuse and split like liquid in plain SwiftUI** (no iOS 26 `GlassEffectContainer`) — e.g. a speed-dial FAB whose actions extrude out of the button — render the blobs in a `Canvas` with the goo filter:760761```swift762Canvas { ctx, size in763 ctx.addFilter(.alphaThreshold(min: 0.5, color: blobColor)) // hard edge after blur764 ctx.addFilter(.blur(radius: 16)) // halos overlap → merge765 ctx.drawLayer { layer in766 layer.fill(Path(ellipseIn: fabRect), with: .color(.white))767 for b in bubbles { layer.fill(Path(ellipseIn: b.rect), with: .color(.white)) }768 }769}770```771772Non-obvious rules — each one is a real trap:773774- **`alphaThreshold` gives a HARD edge.** Blobs are crisp circles at rest, gooey only where they overlap. "Liquid" ≠ "soft/blurry".775- **Animate the Canvas by making the layer `View, Animatable`** with `animatableData = progress`. A raw `@State` read inside the Canvas closure *jumps* — Animatable lets SwiftUI interpolate `progress` frame-by-frame and redraw the goo each step.776- **Keep every blob ~the same size.** One global blur only matches one circle size — if a bubble shrinks (e.g. `0.45·r` mid-travel) the fixed blur ≈ its radius and the threshold collapses it into a **line**. Hold blobs near full size (`0.9–1.0·r`).777- **Rest spacing must exceed `diameter + 2·blur`** or neighbours never clear each other's blurred alpha → permanent **teardrops**. The neck should exist only *during* the transition.778- **Slow the morph** (`spring(response ≈ 1.0)`) — fast springs hide the whole goo.779- **The Canvas is visual only.** Put real `Button`s on top at the same positions for taps + icons. Icons must **travel with their blob on the same spring** (animated `.position`), not `.transition`-pop at the destination, or the motion looks non-uniform. Closed buttons stacked on the FAB need `.allowsHitTesting(false)` so they don't steal its tap.780781**Styling taste:** a liquid FAB reads best **flat** (no shadow). Make blob + icon colors parameters that invert with the background — dark bg → white blob + dark glyphs, light bg → dark blob + white glyphs. `+` → `✕` is just a `135°` rotation of one `plus` symbol; a `.thin` weight makes a premium glyph.782783---784785## Canvas Path Loaders (outline tracing / comet trail)786787For a loader that **traces a shape's outline with a fading comet trail** (infinity, star, polygon, heart, rose…), drive a `Canvas` from a `TimelineView` clock and a *precomputed* set of outline samples — never re-evaluate the shape's math every frame:788789```swift790// Build ONCE — cache unit samples per shape (e.g. in a static dictionary).791let samples: [CGPoint] = makeSamples(for: shape) // arc-length-even points792793TimelineView(.animation(minimumInterval: 1.0/60.0)) { ctx in794 let progress = (ctx.date.timeIntervalSince(start) / duration).wrappedUnit795 Canvas(rendersAsynchronously: true) { gc, size in796 // stroke the static outline faintly …797 // … then draw N trail dots, each at interpolatedPoint(in: samples,798 // progress: progress - tail*trailLength), radius/opacity fading by tail.799 }800}801```802803Non-obvious rules — each one is a real trap:804805- **Sample-then-animate; never eval-per-frame.** Precompute the outline as `[CGPoint]` unit samples (cache them). Animate only a `progress` head that **interpolates along the cached samples**. The renderer (outline stroke + trail) is shape-agnostic — adding a new shape = adding one sample generator, with *zero* changes to the motion code. Keeps "what shape" fully decoupled from "how it moves."806- **Arc-length-even sampling = constant-velocity motion.** Walk the outline at equal *arc-length* steps, not equal parameter `t`. Equal-`t` bunches points where the curve is slow and spreads them where it's fast, so a head advancing by `progress` visibly **speeds up / slows down** (worst around polygon corners). Equal arc-length → the head glides at uniform speed. **Shape sharpness and motion smoothness are independent** — sharp corners in the silhouette do *not* cause jerk in the travel.807- **Two samplers for two silhouettes.** A polyline arc-length sampler (straight edges, crisp vertices → star / polygon) vs a closed **Catmull-Rom** spline (organic flowing curves). Catmull-Rom needs `> 3` control points — fall back to the polyline sampler otherwise. Pick by whether the shape *should* have corners.808- **Shape recipes.** An N-point **star** = `2N` vertices alternating outer radius `1.0` / inner radius `~0.42`; a **regular polygon** = `N` unit-circle vertices; a rotation offset orients it (point-up `-π/2`, flat-top hexagon `+π/6`).809- **Use a `TimelineView` clock, not a spring / `withAnimation`.** A loader is a continuous loop, not a state transition — derive everything from `time`: `progress = (time/duration).wrappedUnit`, plus independent `rotation` and `breathe` from the same clock. (This is the Canvas form of the **Loading Indicator → never spring** rule in the Archetype Catalog.)810811**Content taste:** for a generic loader showcase, prefer **generic geometric primitives** (star, pentagon, hexagon, infinity) over tracing brand/trademark logos — same premium feel, no trademark exposure, and the shapes generalize.812813---814815## Output (Create mode)816817Stream these progress lines one by one:818819```820⚙️ swiftui-microinteractions v1.11.0821🖼️ Assets: <found: name1, name2… · or · none found, using placeholders>822🎯 Archetype: <archetype name>823⚡ Physics: <spring preset and why — one phrase>824🎮 Haptics: <2–3 haptic moments>825🏗️ State: <tier> — <@State var names>826✍️ Writing <FileName>.swift…827```828829**Step 1 — Write the file to disk:**830- Carousel → `legendary-Animo/Carousels/` if exists, else `Carousels/` if exists, else cwd831- Other → `legendary-Animo/Animations/` if exists, else `Animations/` if exists, else cwd832833Print: `✅ Saved to <full relative path>`834835---836837**Step 2 — Xcode project registration:**838839Check for a `.xcodeproj` file:840```bash841find . -maxdepth 3 -name "*.xcodeproj" -type d | head -1842```843844**If `.xcodeproj` found** → register the file in `project.pbxproj` using this Python script:845846```python847import uuid, re848849pbxproj = "<xcodeproj_path>/project.pbxproj"850filename = "<FileName>.swift"851filepath = "<full relative path written above>"852# Determine target group: "Carousels" if saved in Carousels/, else "Animations"853group_name = "Carousels" # or "Animations"854855FILE_REF = uuid.uuid4().hex[:24].upper()856BUILD_FILE = uuid.uuid4().hex[:24].upper()857858with open(pbxproj) as f:859 content = f.read()860861# Find an existing file in the same group as an insertion anchor862# Look for the last .swift entry in PBXFileReference section for that group863# Insert PBXBuildFile, PBXFileReference, group child, and Sources build phase entry864# Use the same indentation and formatting as surrounding entries865```866867Run the script with the actual values. Use an existing same-group file as the anchor for each insertion point. Print:868869```870📦 Registered in Xcode project: <xcodeproj name>871 └─ PBXFileReference ✓872 └─ PBXBuildFile ✓873 └─ Group child ✓874 └─ Sources phase ✓875```876877**If no `.xcodeproj` found** → print:878879```880⚠️ No Xcode project found. Add the file manually:881 File path: <full path>882 In Xcode: File → Add Files to "<ProjectName>"883 or drag into the Navigator → check "Add to target"884```885886---887888**Step 3 — ContentView registration snippet** (```swift block, paste manually):889890Use today's actual date for the `date:` field — never hardcode a past date.891892```swift893DemoItem(894 row: RowView(icon: "💧", title: "Title Here", desc: "Feature · feature · feature"),895 destination: wrappedDestination { YourView() },896 date: "<today's date e.g. June 11, 2026>",897 hasDateHeader: true898)899```900901**Step 4 — Asset notes** (only if placeholders were used):902```903ASSET NOTES:904- Add portrait images to Assets.xcassets/Images/ named: photo1, photo2…905 → Recommended size: 400×600pt, portrait ratio906- Remove placeholder RoundedRectangle once real assets are added907```908909---910911## Output (Edit mode)912913Stream before editing:914```915📂 Reading <FileName>.swift…916✏️ Applying: <one-line summary of change>917✍️ Writing updated file…918```919920Overwrite the file at its existing path, then print:921```922✅ Updated <full relative path>923```924925Then output a **Changes** bullet list of what was modified and why.926(No pbxproj update needed for edits — file is already registered.)927
Full transparency — inspect the skill content before installing.