Motion should explain a state change, and haptics should reinforce a meaningful event. Reduced-motion settings require a deliberate alternative rather than disabling unrelated feedback.

User action → State change → Motion preference → Appropriate feedback
  1. 1User action
  2. 2State change
  3. 3Motion preference
  4. 4Appropriate feedback

Work through the example

Compare the same interaction with reduced motion enabled. Preserve the result and focus order even if the transition changes.

Start with a disposable branch and synthetic data. Write the expected outcome before changing the implementation, then keep the first failing result. This prevents a later repair from quietly redefining the task. The procedure below is grounded in the repository reference; its examples must still be checked against your project and installed toolchain.

Implementation reference

The following focused section is adapted from the maintained project guide. It preserves the source’s examples and limitations.

1. Animation Standards

Default Curves and Durations

Category Duration Curve Usage
Micro interaction 0.2s .easeOut Toggles, button presses, icon changes
Navigation transition 0.35s .spring(response: 0.35, dampingFraction: 0.85) Push, pop, tab switches
Content loading 0.3s .easeInOut Skeleton to content, fade-in
Dismissal 0.25s .easeIn Sheet dismiss, alert close, toast exit
Bouncy spring -- .bouncy Playful UI: reactions, badges, celebrations
Snappy spring -- .snappy Responsive controls: sliders, toggles
Smooth spring -- .smooth Elegant reveals: cards, overlays

When to Use Which Animation

Use Case Animation Rationale
Button tap feedback .easeOut, 0.2s Quick acknowledgment, no lingering
Toggle switch .snappy Responsive mechanical feel
Card expand/collapse .spring(response: 0.35, dampingFraction: 0.85) Natural, physical motion
Pull-to-refresh .bouncy Playful rubber-band feel
Modal presentation .smooth Elegant, unhurried entrance
Error shake .default.repeatCount(3) Attention-grabbing without being jarring
Skeleton shimmer .easeInOut, 1.2s, repeat Smooth continuous loop
Item deletion .easeIn, 0.25s Quick exit, attention moves forward
List reorder .snappy Keeps up with the finger
Hero transition matchedGeometryEffect Spatial continuity between screens

Transition Standards

// MARK: - Sheet Presentation (use system default)
.sheet(isPresented: $showSettings) {
    SettingsView()
}

// MARK: - Full Screen Cover with Custom Transition
.fullScreenCover(isPresented: $showOnboarding) {
    OnboardingView()
        .transition(.opacity.combined(with: .move(edge: .bottom)))
}

// MARK: - Navigation Push (system default)
NavigationStack {
    List(items) { item in
        NavigationLink(value: item) {
            ItemRow(item: item)
        }
    }
    .navigationDestination(for: Item.self) { item in
        ItemDetailView(item: item)
    }
}

// MARK: - Hero Transition with matchedGeometryEffect
struct HeroTransitionExample: View {
    @Namespace private var heroNamespace
    @State private var isExpanded = false

    var body: some View {
        if isExpanded {
            DetailCard(namespace: heroNamespace)
                .onTapGesture {
                    withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
                        isExpanded = false
                    }
                }
        } else {
            ThumbnailCard(namespace: heroNamespace)
                .onTapGesture {
                    withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
                        isExpanded = true
                    }
                }
        }
    }
}

struct ThumbnailCard: View {
    var namespace: Namespace.ID

    var body: some View {
        RoundedRectangle(cornerRadius: 12)
            .fill(.blue.gradient)
            .matchedGeometryEffect(id: "card", in: namespace)
            .frame(width: 120, height: 120)
            .overlay {
                Text("Tap")
                    .matchedGeometryEffect(id: "title", in: namespace)
            }
    }
}

struct DetailCard: View {
    var namespace: Namespace.ID

    var body: some View {
        RoundedRectangle(cornerRadius: 24)
            .fill(.blue.gradient)
            .matchedGeometryEffect(id: "card", in: namespace)
            .frame(maxWidth: .infinity, maxHeight: 400)
            .overlay {
                Text("Detail View")
                    .matchedGeometryEffect(id: "title", in: namespace)
            }
            .padding()
    }
}

// MARK: - Custom Asymmetric Transition
extension AnyTransition {
    static var slideAndFade: AnyTransition {
        .asymmetric(
            insertion: .move(edge: .trailing).combined(with: .opacity),
            removal: .move(edge: .leading).combined(with: .opacity)
        )
    }
}

// Usage:
struct CustomTransitionExample: View {
    @State private var showContent = false

    var body: some View {
        VStack {
            if showContent {
                ContentView()
                    .transition(.slideAndFade)
            }
            Button("Toggle") {
                withAnimation(.easeInOut(duration: 0.3)) {
                    showContent.toggle()
                }
            }
        }
    }
}

// MARK: - Phased Animation for Multi-Step Effects
struct PhasedAnimationExample: View {
    @State private var trigger = false

    var body: some View {
        Image(systemName: "bell.fill")
            .font(.system(size: 32))
            .phaseAnimator([false, true], trigger: trigger) { content, phase in
                content
                    .scaleEffect(phase ? 1.2 : 1.0)
                    .rotationEffect(.degrees(phase ? 15 : 0))
            } animation: { phase in
                phase ? .bouncy : .snappy
            }
            .onTapGesture { trigger.toggle() }
    }
}

Acceptance and failure review

Checkpoint What to inspect If it does not match
User action Confirm the input and environment Preserve the failure and return to this step
State change Inspect the intermediate artifact Preserve the failure and return to this step
Motion preference Run the focused check Preserve the failure and return to this step
Appropriate feedback Record the observed result Preserve the failure and return to this step

Ask the agent to explain the smallest change that resolves the observed mismatch. Keep unrelated refactors out of the repair. A change that makes a warning disappear is not enough if the behavior or ownership contract has changed. Re-run the same acceptance check so the before and after results are comparable.

Evidence and limits

No physical-device haptic evaluation is included in the simulator baseline.

This is an educational guide. Its presence in the series does not certify a completed client-specific lab. The series evidence record separates executed checks from exercises and blocked environments.

Inspect the source used in this lesson.

What to do next

Next: Making an agent-built app look intentional, not templated