From ios-dev-toolkit
Review an iOS/SwiftUI feature and enhance it with purposeful animations, micro-interactions, haptics, and motion that improve usability and delight. Uses SwiftUI springs, matched geometry, symbol effects, and gesture-driven animation. Use this skill whenever the user mentions adding animation, transitions, micro-interactions, motion design, making a view feel alive, or wants an iOS screen to feel more fluid and responsive. Also use when things feel "static", "abrupt", "janky", or "lifeless" in an iOS app.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ios-dev-toolkit:ios-animateThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are adding purposeful motion to a SwiftUI interface. Motion on iOS should feel like physics —
You are adding purposeful motion to a SwiftUI interface. Motion on iOS should feel like physics — objects have mass, momentum, and settle naturally. The goal is not "add animations" but "make the interface feel alive and responsive."
Read every file in scope, then identify:
State findings briefly, then plan.
Before writing code, decide:
The cardinal rule: One well-orchestrated experience beats scattered animations everywhere.
Work through these categories systematically. Skip what's not relevant.
Always use springs. Never use .easeInOut or .linear.
Springs feel physical because they model real-world physics — objects don't move in cubic bezier curves. iOS users are trained on spring animations from every system interaction.
| Spring | Feel | Use For |
|---|---|---|
.spring(.snappy) | Quick, precise, professional | Button taps, toggles, selection changes, filter state |
.spring(.smooth) | Fluid, natural, unhurried | Layout changes, expanding sections, sheet presentations |
.spring(.bouncy) | Playful, energetic, celebratory | Task completion, achievements, onboarding, empty→content |
.spring(.snappy, extraBounce: 0.1) | Snappy with subtle life | Cards, chips, interactive drag elements |
// Quick feedback
withAnimation(.spring(.snappy)) { isSelected.toggle() }
// Layout change
withAnimation(.spring(.smooth)) { isExpanded.toggle() }
// Celebration
withAnimation(.spring(.bouncy)) { showConfetti = true }
| Category | Duration | Examples |
|---|---|---|
| Micro-feedback | 100-200ms | Toggle, checkbox, button press, haptic |
| State changes | 200-350ms | Filter change, tab switch, content reveal |
| Layout transitions | 300-500ms | Section expand, sheet present, keyboard |
| Hero transitions | 400-600ms | Navigation push, zoom transition |
| Celebratory | 500-800ms | Task complete, achievement, confetti |
Nothing over 500ms for standard UI. Longer durations are only for moments of delight.
Users adding content are building anticipation. Users dismissing content want it gone.
.transition(.asymmetric(
insertion: .opacity.combined(with: .scale(0.9)).animation(.spring(.smooth)),
removal: .opacity.animation(.spring(.snappy))
))
Button feedback:
// Scale feedback on press (use ButtonStyle, not manual animation)
struct ScaleButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.96 : 1)
.animation(.spring(.snappy), value: configuration.isPressed)
}
}
Toggle/selection haptics:
Toggle("Notifications", isOn: $enabled)
.sensoryFeedback(.selection, trigger: enabled)
Action confirmation haptics:
Button("Complete Task") { completeTask() }
.sensoryFeedback(.success, trigger: taskCompleted)
Error haptics:
Button("Delete") { attemptDelete() }
.sensoryFeedback(.error, trigger: deleteFailed)
Show/hide content:
if showDetail {
DetailView()
.transition(.opacity.combined(with: .move(edge: .bottom)))
}
// Trigger with: withAnimation(.spring(.snappy)) { showDetail.toggle() }
Expand/collapse:
VStack {
Button { withAnimation(.spring(.smooth)) { isExpanded.toggle() } } label: {
HStack {
Text("Details")
Image(systemName: "chevron.right")
.rotationEffect(.degrees(isExpanded ? 90 : 0))
}
}
if isExpanded {
ExpandedContent()
.transition(.opacity.combined(with: .scale(0.95, anchor: .top)))
}
}
Loading → content:
if isLoading {
PlaceholderView()
.redacted(reason: .placeholder)
.transition(.opacity)
} else {
ContentView()
.transition(.opacity.combined(with: .scale(0.98)))
}
// Animate the state change:
// withAnimation(.spring(.smooth)) { isLoading = false }
Zoom transition (iOS 18+):
// Source
NavigationLink(value: item) {
ItemRow(item)
.matchedTransitionSource(id: item.id, in: namespace)
}
// Destination (in .navigationDestination)
ItemDetail(item)
.navigationTransition(.zoom(sourceID: item.id, in: namespace))
Matched geometry within a view:
// For elements that move between states (e.g., selected indicator)
RoundedRectangle(cornerRadius: 8)
.fill(Color.accentColor)
.matchedGeometryEffect(id: "selection", in: namespace)
Numeric text (counters, scores, badges):
Text(count, format: .number)
.contentTransition(.numericText())
.animation(.spring(.snappy), value: count)
Symbol replacement:
Image(systemName: isPlaying ? "pause.fill" : "play.fill")
.contentTransition(.symbolEffect(.replace))
// Bounce on event (tap confirmation, "added")
Image(systemName: "checkmark.circle.fill")
.symbolEffect(.bounce, value: trigger)
// Pulse for ongoing activity (syncing, recording)
Image(systemName: "antenna.radiowaves.left.and.right")
.symbolEffect(.pulse)
// Variable color for progress (wifi strength, upload)
Image(systemName: "wifi")
.symbolEffect(.variableColor.iterative)
// Wiggle for attention (notification, error)
Image(systemName: "bell.fill")
.symbolEffect(.wiggle, value: hasNotification)
ForEach(Array(items.enumerated()), id: \.element.id) { index, item in
ItemRow(item)
.opacity(appeared ? 1 : 0)
.offset(y: appeared ? 0 : 20)
.animation(
.spring(.snappy).delay(Double(index) * 0.05),
value: appeared
)
}
.onAppear { appeared = true }
Rules: 50-100ms stagger per item. Cap total stagger at ~500ms (first 5-8 items stagger, rest appear together).
.gesture(
DragGesture()
.onChanged { value in
// Follow finger — no animation needed, direct tracking
offset = value.translation.height
}
.onEnded { value in
// Snap to target with spring — this is the satisfying part
withAnimation(.spring(.snappy)) {
offset = value.translation.height > threshold ? targetOffset : 0
}
}
)
Haptics are the invisible animation — they make interactions feel physical.
| Haptic | Use For |
|---|---|
.sensoryFeedback(.selection, trigger:) | Picks, toggles, filter changes, tab switches |
.sensoryFeedback(.impact(.light), trigger:) | Subtle taps, drag snaps, scroll stops |
.sensoryFeedback(.impact(.medium), trigger:) | Confirmations, sends, drops |
.sensoryFeedback(.success, trigger:) | Task complete, save, achievement |
.sensoryFeedback(.error, trigger:) | Failed action, invalid input |
.sensoryFeedback(.warning, trigger:) | Destructive action confirmation |
Rules: Key moments only. Haptics on every scroll event = bad. Haptics on task completion = good.
@Environment(\.accessibilityReduceMotion) private var reduceMotion
// Decorative animations: remove
withAnimation(reduceMotion ? .none : .spring(.snappy)) {
showContent = true
}
// Functional transitions: simplify (cross-fade instead of spatial movement)
.transition(reduceMotion ? .opacity : .opacity.combined(with: .move(edge: .bottom)))
Rule of thumb: Reduce means reduce, not remove. Opacity changes are usually fine. Spatial movement and bouncing are what causes discomfort.
After implementing, check:
.easeInOut, .linear, or .default)accessibilityReduceMotion checked for all decorative motion.easeInOut or .linear — always springs.animation(_, value:) on a parent that animates everything — be surgicalprefers-reduced-motion — this is an accessibility violationTask.sleep(nanoseconds:) for animation delays — use .delay() or Task.sleep(for:)npx claudepluginhub elvinouyang/claude-skill-collection --plugin ios-dev-toolkitWrites Swift animation code using SwiftUI, UIKit, and Core Animation for iOS apps. Covers iOS 18-26 APIs like KeyframeAnimator, PhaseAnimator, matchedGeometryEffect for transitions, gestures, and design implementations.
Guides SwiftUI animation patterns including implicit/explicit animations, transitions, phase/keyframe animations, Animatable protocol, and @Animatable macro. Use when implementing motion or transitions in views.
Implement, review, or improve SwiftUI animations and transitions: explicit/implicit animations, spring curves, phase/keyframe animators, hero transitions, SF Symbol effects, custom animations, and accessibility support.