| name | ios-gestures-and-haptics |
| description | iOS gestures and haptics applied — the standard system gestures (tap, long-press, swipe, pan, pinch, rotate, edge-swipe back) and the rule never to override them, the SwiftUI gesture APIs (.onTapGesture / LongPressGesture / DragGesture / MagnifyGesture / RotateGesture and composing them) plus the row-level affordances (.swipeActions, .contextMenu, .draggable/.dropDestination, .refreshable), and the haptics stack — the three UIFeedbackGenerator families (impact / notification / selection), Core Haptics for custom patterns, and SwiftUI .sensoryFeedback(_:trigger:) — with HIG best practices. Use when adding any gesture, swipe action, or haptic to a native iOS screen. |
| tags | ["ios","gestures","haptics","swiftui","hig","interaction"] |
iOS gestures & haptics — system gestures, feedback generators, and Core Haptics
Source: Apple HIG Gestures + Playing haptics, developer.apple.com Core Haptics / UIFeedbackGenerator / SwiftUI gesture + .sensoryFeedback docs, and WWDC 2026 (What's new in SwiftUI, Build powerful drag and drop in SwiftUI, session 271) + the WWDC26 SwiftUI guide, cross-checked across two research passes (June 2026). Currency: iOS 26 = shipping baseline; iOS 27 (WWDC 2026) = current cycle — iOS 27 broadens drag-to-reorder and swipe actions to any container (§3); haptics are unchanged from iOS 17. Apple says: honor standard gestures, reserve haptics for meaningful moments, and always pair a haptic with a visible change.
1. Standard system gestures — learn them, don't fight them
People expect these to mean the same thing in every app. Never repurpose or block them — overriding the back-swipe or pull-to-refresh is the fastest way to feel broken.
| Gesture | Standard meaning |
|---|
| Tap | Activate a control / select an item |
| Double-tap | Zoom in & center (or zoom out if zoomed) |
| Long-press (touch & hold) | Reveal contextual actions / edit mode / preview |
| Swipe (1 finger) | Scroll; reveal a row's delete/actions; dismiss |
| Edge-swipe from left | Back — pop the navigation stack |
| Pan / drag | Move an element or reorder (iOS 27: .reorderable() brings drag-to-reorder to any container — see §3) |
| Pinch | Zoom (out = in, in = out) |
| Rotate (two-finger) | Rotate content |
| Flick | Fast scroll / pan |
| Two/multi-finger | System & accessibility (scroll, select, VoiceOver) — leave alone |
Rules: custom gestures must be discoverable (show an affordance or onboard them — a hidden gesture is invisible); keep them additive, never the only path to an action; don't put a custom gesture where the system already owns that motion (e.g. a horizontal swipe inside a row that also wants edge-back).
2. SwiftUI gesture APIs
.onTapGesture { tap() }
.onLongPressGesture(minimumDuration: 0.5) { … }
@GestureState private var drag = CGSize.zero
someView.gesture(
DragGesture()
.updating($drag) { value, state, _ in state = value.translation }
.onEnded { value in commit(value.translation) }
)
MagnifyGesture()
RotateGesture()
Composing (pick the relationship explicitly):
.simultaneously(with:) — both at once (pinch and rotate a photo).
.sequenced(before:) — second only fires after the first succeeds (long-press then drag = pick-up-and-move).
.exclusively(before:) — first wins if it can; otherwise the second.
.highPriorityGesture(…) beats the view's own gestures; .simultaneousGesture(…) runs alongside them. Use these when a child view (e.g. a scroll view or button) would otherwise swallow the touch.
3. Row & container affordances (prefer these over hand-rolled gestures)
These are pre-tuned, accessible, and respect system gestures — reach for them before a raw DragGesture.
row
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) { delete() } label: { Label("Delete", systemImage: "trash") }
}
.swipeActions(edge: .leading) { Button { repeatLog() } label: { Label("Repeat", systemImage: "arrow.clockwise") } }
.contextMenu { Button { edit() } label: { Label("Edit", systemImage: "pencil") } }
.draggable(item)
List { … }.refreshable { await reload() }
dropZone.dropDestination(for: FoodItem.self) { items, _ in accept(items) }
A leading destructive .swipeActions button with allowsFullSwipe reproduces Mail's swipe-to-delete for free — don't rebuild it.
iOS 27 (WWDC 2026): these now work outside List
The two List-only affordances above became container-agnostic — same code in a LazyVGrid, LazyVStack, ScrollView, or custom Layout.
ScrollView {
LazyVGrid(columns: cols) {
ForEach(meals) { MealCell($0) }
.reorderable()
}
}
.reorderContainer(for: Meal.self) { diff in
diff.apply(to: &meals)
}
ScrollView {
LazyVStack { ForEach(rows) { RowView($0).swipeActions(edge: .trailing) { … } } }
}
.swipeActionsContainer()
- Replaces the old reorder path (
.onMove(perform:), which was List/Form-only and EditMode-bound) — .reorderable() needs no edit mode and rides the new drag-and-drop system. Reordering also reaches watchOS for the first time; unavailable on tvOS.
- Verify-at-source flag: confirm the exact
.reorderContainer(for:) closure shape (CollectionDifference apply) and whether .reorderable() takes any arguments against the Xcode 27 SDK before relying on signatures.
4. Haptics layer 1 — UIFeedbackGenerator (use this 90% of the time)
Three families. Match the semantics, not the feel.
| Family | Variants | Use for |
|---|
Impact (UIImpactFeedbackGenerator) | .light .medium .heavy .soft .rigid | A UI element collides / snaps / locks into place |
Notification (UINotificationFeedbackGenerator) | .success .warning .error | The outcome of a task (saved, flagged, failed) |
Selection (UISelectionFeedbackGenerator) | (single tick) | Discrete value change while scrubbing a picker/stepper |
UINotificationFeedbackGenerator().notificationOccurred(.success)
let g = UIImpactFeedbackGenerator(style: .soft); g.prepare(); g.impactOccurred()
let sel = UISelectionFeedbackGenerator(); sel.selectionChanged()
Call .prepare() just before the moment to cut latency. Standard controls (toggle, slider, picker, .swipeActions) already play haptics on supported iPhones — don't double up.
5. Haptics layer 2 — SwiftUI .sensoryFeedback (iOS 17+, prefer in SwiftUI)
Declarative wrapper over the generators — fires when trigger changes; no generator lifecycle to manage.
.sensoryFeedback(.success, trigger: didSaveLog)
.sensoryFeedback(.selection, trigger: servings)
.sensoryFeedback(.impact(weight: .light), trigger: tappedTile)
.sensoryFeedback(.warning, trigger: dayCalories) { _, new in new > target }
.sensoryFeedback(trigger: saveResult) { _, new in new.isFailure ? .error : .success }
Cases: .success .warning .error .selection .increase .decrease .impact(weight:flexibility:intensity:). (.start/.stop are watchOS-only; .alignment/.levelChange macOS-only; iPad has no haptics — code so the absence is silent, never a missing-confirmation bug.)
6. Haptics layer 3 — Core Haptics (only for custom textures)
Use only when the three families can't express it (a tailored sequence, audio-synced taps, intensity that tracks a value). Gate on hardware and own the engine lifecycle.
guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return }
let engine = try CHHapticEngine(); try engine.start()
let tap = CHHapticEvent(eventType: .hapticTransient, parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.9),
CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
], relativeTime: 0)
let pattern = try CHHapticPattern(events: [tap], parameters: [])
try engine.makePlayer(with: pattern).start(atTime: CHHapticTimeImmediate)
- Transient = a single crisp tap; continuous = a sustained buzz (
duration:), parameter-curve it for swells.
- Reusable patterns can live in an AHAP file (JSON of events/parameters) and load by name.
- Restart the engine on
.stopped / .reset handlers — iOS suspends it in the background.
7. Haptic best practices (HIG)
- Always pair with a visible change — a haptic with no on-screen effect feels like a glitch.
- Match intensity to importance —
.light/.selection for incidental, .success/.error for outcomes, .heavy rarely. Escalating everything trains users to ignore it.
- Never spam — one haptic per discrete event, not per pixel of a drag; debounce continuous changes.
- Respect the user — System Haptics can be off; haptics fire only on the foreground app on capable iPhones. Treat haptics as enhancement, never the sole signal of success or error (also show/sound it).
.prepare() before time-critical taps to avoid a perceptible delay.
8. Mistakes that read as cheap / amateur
Overriding the edge-swipe back or pull-to-refresh · a hidden custom gesture with no affordance · haptic with no matching visual · .heavy/.success fired on every tap (alarm fatigue) · haptics inside a tight drag/scroll loop · reaching for Core Haptics when .impact(.soft) would do · forgetting iPad/older devices have no haptics and shipping it as the only confirmation · using .error for a benign event · re-implementing swipe-to-delete by hand instead of .swipeActions.
9. Greg application
- Logged-meal rows:
.swipeActions(edge: .trailing, role: .destructive) for delete; .contextMenu for Edit/Duplicate on long-press. Don't hand-roll either — and keep the row's horizontal swipe from fighting the edge-back gesture. On iOS 27, if the log list ever moves off List into a LazyVStack/ScrollView (e.g. a denser custom layout), add .swipeActionsContainer() to the scroll view to keep swipe-to-delete working, and .reorderable() + .reorderContainer(for:) if rows ever need reordering — no EditMode plumbing.
- Log-confirm:
.sensoryFeedback(.success, trigger: didLog) the instant a meal commits, paired with the tile's color-block settle — outcome haptic, once.
- Over-target (the adaptation trigger): when a log pushes the day over the calorie target,
.sensoryFeedback(.warning, trigger: dayCalories) { _, n in n > target } alongside the macro tile turning its warning color — the haptic reinforces the visual, never replaces it.
- Serving stepper (black photo-detail review screen):
.sensoryFeedback(.selection, trigger: servings) so each tick lands like a native picker; respect the system back-swipe out of that detail screen.
- Restraint: Today/Progress are glanceable — no haptics on scroll, chart render, or passive metric updates. Reserve feel for commit, over-target, and stepper. No Core Haptics in v0; the three families cover every Greg moment.
Pairs with: ios-components (the controls these gestures live on), ios-motion-and-animation (the visual that must accompany each haptic), multimodal-voice-and-haptics (cross-modal feedback strategy), ios-flows-and-patterns (where in a flow each affordance belongs).