Hydrolyze Micro Interaction System

Concept · refreshed Search related

Three reusable utilities in Hydrolyze/Sources/Lib/ plus the

design vocabulary they enforce. The technical architecture behind

the 2026-06-20 audit + implementation. Established as the canonical

layer for all haptic, press, and motion work in the iOS app.

The three utilities

1. Haptics.swift — semantic haptic vocabulary

One file, one vocabulary. All generators pre-warmed and reused (constructing UIImpactFeedbackGenerator per-call is wasteful and delays the first haptic).

APITypeUse case
Haptics.selection()SoftestFilter pills, tab switches, segmented control changes. Frequent, quiet.
Haptics.tap()LightButton presses in lists, secondary actions, row taps. The "I tapped a thing" signal.
Haptics.action()MediumThe LAP button, primary CTAs, accept/confirm. The "I did the thing" signal.
Haptics.heavy()HeavyDestructive actions (delete, discard session), STOP during timing.
Haptics.success()NotificationForm submission success, session save complete.
Haptics.warning()NotificationLow-confidence confirmation, "are you sure?" before destructive ops.
Haptics.error()NotificationSave failure, network error, auth error.

The discipline: never construct UIImpactFeedbackGenerator at call sites. If a new haptic style is needed, add a named case here so the app's tactile language stays coherent.

2. PressFeedbackStyle.swift — visible press states

Default iOS button rendering shows a subtle gray opacity change on press. For a 200×200 cyan LAP button, that opacity tick is invisible — the button is so big the opacity change is dwarfed by the surface. A scale-down + slight dim is what tells the user "your tap registered."

Three styles:

secondary actions, list rows, nav chrome

200pt START button, the 160pt LAP button). More obvious confirmation.

that aren't Buttons (e.g., a card with .onTapGesture)

Both styles use spring physics (response 0.15 on press, 0.35 on release) so the press "settles" instead of snapping.

3. Animations.swift — shared motion curves

APICurveUse case
Animations.snapeaseInOut(0.18s)Filter tab switch, toggle, button press
Animations.gentleeaseInOut(0.35s)Row fades in, filter list updates, empty state appears
Animations.springSnappyspring(response: 0.30, damping: 0.60)LAP flash, row insert, success state. Has a small overshoot.
Animations.springSoftspring(response: 0.45, damping: 0.80)State transitions, page-level. No overshoot — settles.
Animations.breathingeaseInOut(1.2s)"I exist, tap me" invitation pulses on idle CTAs.

The discipline: never write .easeInOut(duration: 0.2) at call sites. Use the named curves here so the motion language stays coherent.

Why three utilities (not five, not one)

The instinct was to make a single MicroInteractions namespace that bundled everything. The split is deliberate:

to do with SwiftUI rendering. Keeping it separate means non-view code (timing, model layer) can call haptics without dragging the SwiftUI runtime.

ViewModifier. Splitting it from haptics means a view can use one without the other (e.g., a destructive button that has heavy haptic + scale-down; a passive tappable card that has press feedback but no haptic).

not a behavior. The same Animations.snap is used in .animation(_:value:), withAnimation(_:), and .transition(_:).

The split also keeps each file <120 lines — easy to read, easy to extend, easy to test independently.

Design principles (what the system enforces)

  1. One vocabulary per file. No ad-hoc haptics or animation

curves outside the utility files. If a new case is needed, add it to the utility, don't bypass.

  1. Pre-warm the generators. Haptics.swift initializes all

generators as static let and calls .prepare() after every fire. The first haptic is never delayed.

  1. Spring over ease for primary actions. The LAP tap is the

most-tapped button in the app. Animations.springSnappy on the cyan flash + medium haptic + StrongPressFeedbackStyle(scale: 0.90) is the "this is the right tap" feedback triad. Use it consistently.

  1. Breathing is for invitation, not notification. The 200pt

START button breathes (Animations.breathing, 1.04 scale) so the coach sees it wanting to be tapped. NEVER use breathing on error states, warnings, or anything that needs attention — breathing reads as "alive and waiting," not "alert."

  1. Haptic intensity matches semantic weight. Selection < tap <

action < heavy. The coach gets medium on LAP (their primary action), heavy on STOP (deliberately heavier because it's a state change), and success on save-complete. The hierarchy is felt in the hand.

Cross-cutting rules

→ medium haptic + spring flash + scale-down. All three together. Picking only one is half the feedback.

user.** Haptics.selection() is the only haptic that's safe to fire on every segmented control change without being intrusive. Reserve the others for actions that have consequences.

TimingView aesthetic.** Per the Phase 2.5-c design rule, the timing screen is B&W + 20% cyan. The flash is the one place where cyan fills the screen (at 40% opacity, spring-decayed). Don't add more cyan saturation elsewhere.

iOS version requirements

Animation.spring() defaults are different.

See also

direction that motivated this system

2026-06-20 milestone

rich effortlessness" principle; the playful direction is an extension, not a replacement

— Felt Faction citing Hydrolyze haptic patterns to lift

naturally with haptics at the iOS layer

Source

Hydrolyze/Sources/Lib/PressFeedbackStyle.swift, Hydrolyze/Sources/Lib/Animations.swift

Animations usages across 11 view files (74 interaction points total)

Published and managed by TARS, an AI co-author built on Nathan's gbrain.