Swipe Trap
Status: ready
Symptom
An important action exists only as a swipe, drag, long press or pinch. A touch user may find it. A VoiceOver, Switch Control, Voice Control or keyboard user can't perform it.
Task: remove an item from favorites
Only path: custom swipe left on the row
That's not a shortcut. That's the only door, and it's hidden.
Where it shows up
Delete or archive on a row, reordering, revealing hidden actions, map zoom, onboarding carousels, card dismiss, drag to confirm.
Why it blocks the task
VoiceOver changes what swipes mean: a one-finger swipe moves focus, it doesn't reach the app. Switch Control and Voice Control can't perform a custom path gesture like a finger. A user with a tremor may not manage a long press or a precise drag. If the gesture is the only path, the task is impossible for these users.
Root cause
The team designed for one input method.
- A custom
DragGesturereplaces the system swipe actions, and nothing else exposes the action. - Drag-to-reorder is the only way to change the order.
- The map zooms only by pinch.
- A long press opens a menu that has no visible button.
- QA tested with a finger only.
Before
FavoriteRow(tea)
.gesture(
DragGesture().onEnded { value in
if value.translation.width < -80 { removeFavorite(tea) }
}
)
VoiceOver focuses the row and has no way to remove it.
Fix
Verdict: the gesture stays as a shortcut. The same action is also a real control or an accessibility action. Which one depends on how often the action is used.
Why: touch users keep the fast path. Everyone else gets a path that doesn't depend on finger movement.
| Action | Alternative |
|---|---|
| Primary, frequent (add to cart) | Visible button |
| Secondary, several per row (archive, share, delete) | Visible "More" menu, or accessibility actions |
| Secondary, one per row | Accessibility action |
| Pinch, drag on a map or a chart | Buttons (Zoom in, Zoom out) or a list (Map Dead End) |
System swipeActions in a List are already exposed to VoiceOver in the Actions rotor. Use them instead of a custom drag, and don't add a duplicate accessibilityAction:
List(favorites) { tea in
FavoriteRow(tea)
.swipeActions {
Button("Remove", role: .destructive) { removeFavorite(tea) }
}
}
A custom gesture needs the action added explicitly:
FavoriteRow(tea)
.gesture(swipeToRemove(tea))
.accessibilityAction(named: "Remove from favorites") { removeFavorite(tea) }
Several secondary actions, visible to everyone:
Menu {
Button("Share") { share(tea) }
Button("Remove from favorites", role: .destructive) { removeFavorite(tea) }
} label: {
Label("More actions for \(tea.name)", systemImage: "ellipsis.circle")
}
A destructive action reached by an accessibility action gets the same confirmation or undo as the swipe.
Check
Profile: VoiceOver
Task: remove a tea from favorites
Expected: Actions rotor on the row offers "Remove", once
undo or confirmation is available
Profile: Switch Control
Task: the same
Expected: reachable without a drag, pinch or long press
Profile: Voice Control
Task: "Tap More actions for jasmine tea", then "Tap Remove from favorites"
Expected: both commands work
WCAG mapping
2.5.1 Pointer Gestures: path-based gestures have a single-pointer alternative.2.1.1 Keyboard: the action is operable without touch.2.5.7 Dragging Movements: a drag has an alternative without dragging.