docs: complete project research for itinerary editor
Files: - STACK.md - UITableView drag-drop APIs, SwiftUI bridging patterns - FEATURES.md - Table stakes UX (lift, insertion line, haptics), polish features - ARCHITECTURE.md - 5-layer semantic positioning architecture - PITFALLS.md - Critical pitfalls (row vs semantic, travel as structural) - SUMMARY.md - Executive synthesis with roadmap implications Key findings: - Stack: UITableView + UIHostingConfiguration (existing pattern validated) - Architecture: Semantic (day, sortOrder) model, not row indices - Critical pitfall: Row indices are ephemeral; semantic positions are truth Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
243
.planning/research/FEATURES.md
Normal file
243
.planning/research/FEATURES.md
Normal file
@@ -0,0 +1,243 @@
|
||||
# Features Research: Drag-Drop Editor UX
|
||||
|
||||
**Domain:** Drag-and-drop itinerary editor for iOS sports travel app
|
||||
**Researched:** 2026-01-18
|
||||
**Confidence:** HIGH (multiple authoritative sources cross-verified)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Polished drag-drop requires deliberate visual feedback at every state transition. The difference between "feels broken" and "feels delightful" comes down to: lift animation, predictable insertion indicators, smooth reshuffling, and magnetic snap-to-place. Your constraints (fixed day headers, fixed games, movable travel/custom items) add complexity but are achievable with proper drop zone logic.
|
||||
|
||||
---
|
||||
|
||||
## Table Stakes
|
||||
|
||||
Features users expect. Missing any of these makes the editor feel broken.
|
||||
|
||||
| Feature | Why Expected | Complexity | Implementation Notes |
|
||||
|---------|--------------|------------|---------------------|
|
||||
| **Lift animation on grab** | Users expect physical metaphor - picking up an object | Low | Elevation (shadow), scale 1.02-1.05x, slight z-offset |
|
||||
| **Ghost/placeholder at origin** | Shows where item came from, reduces anxiety | Low | Semi-transparent copy or outlined placeholder in original position |
|
||||
| **Insertion indicator line** | Must show exactly where item will drop | Medium | Horizontal line with small terminal bleeds, appears between items |
|
||||
| **Items move out of the way** | Preview of final state while dragging | Medium | ~100ms animation, triggered when dragged item center overlaps edge |
|
||||
| **Magnetic snap on drop** | Satisfying completion, confirms action worked | Low | 100ms ease-out animation to final position |
|
||||
| **Clear invalid drop feedback** | Don't leave user guessing why drop failed | Low | Item animates back to origin if dropped in invalid zone |
|
||||
| **Touch hold delay (300-500ms)** | Distinguish tap from drag intent | Low | iOS standard; prevents accidental drags |
|
||||
| **Haptic on grab** | Tactile confirmation drag started | Low | UIImpactFeedbackGenerator.light on pickup |
|
||||
| **Haptic on drop** | Tactile confirmation action completed | Low | UIImpactFeedbackGenerator.medium on successful drop |
|
||||
| **Scroll when dragging to edge** | Lists longer than viewport need auto-scroll | Medium | Scroll speed increases closer to edge, ~40px threshold |
|
||||
|
||||
### Insertion Indicator Details
|
||||
|
||||
The insertion line is critical. Best practices:
|
||||
- Appears **between** items (in the gap), not on top
|
||||
- Has small terminal bleeds (~4px) extending past item edges
|
||||
- Triggered when center of dragged item crosses edge of potential neighbor
|
||||
- Color should contrast clearly (system accent or distinct color)
|
||||
|
||||
### Animation Timing
|
||||
|
||||
| Event | Duration | Easing |
|
||||
|-------|----------|--------|
|
||||
| Lift (pickup) | 150ms | ease-out |
|
||||
| Items shuffling | 100ms | ease-out |
|
||||
| Snap to place (drop) | 100ms | ease-out |
|
||||
| Return to origin (cancel) | 200ms | ease-in-out |
|
||||
|
||||
---
|
||||
|
||||
## Nice-to-Have
|
||||
|
||||
Polish features that delight but aren't expected.
|
||||
|
||||
| Feature | Value | Complexity | Notes |
|
||||
|---------|-------|------------|-------|
|
||||
| **Slight tilt on drag (2-3 degrees)** | Trello's signature polish; makes interaction feel playful | Low | Rotate3D effect, matches brand personality |
|
||||
| **Progressive drop zone highlighting** | Visual intensifies as item approaches valid zone | Medium | Background color change, border enhancement |
|
||||
| **Multi-item drag with count badge** | Power users moving multiple items at once | High | Not needed for v1; itinerary items are usually moved one at a time |
|
||||
| **Keyboard reordering (a11y)** | Up/Down arrows via rotor actions | Medium | Important for accessibility; add accessibilityActions |
|
||||
| **Undo after drop** | Recover from mistakes | Medium | Toast with "Undo" button, ~5 second timeout |
|
||||
| **Drag handle icon** | Visual affordance for draggability | Low | 6-dot grip icon (Notion-style) or horizontal lines |
|
||||
| **Cancel drag with escape/shake** | Quick abort | Low | Shake-to-cancel on iOS; return to origin |
|
||||
| **Drop zone "ready" state** | Zone visually activates before item enters | Low | Subtle background shift when drag starts |
|
||||
|
||||
### Tilt Animation (Trello-style)
|
||||
|
||||
The 2-3 degree tilt on dragged items is considered "gold standard" polish:
|
||||
- Adds personality without being distracting
|
||||
- Reinforces physical metaphor (picking up a card)
|
||||
- Should match your app's design language (may be too playful for some apps)
|
||||
|
||||
---
|
||||
|
||||
## Overkill
|
||||
|
||||
Skip these - high complexity, low value for an itinerary editor.
|
||||
|
||||
| Feature | Why Skip | What to Do Instead |
|
||||
|---------|----------|-------------------|
|
||||
| **Drag between sections/screens** | Your items live within days; cross-day moves are rare | Allow within same list only, or use "Move to..." action menu |
|
||||
| **Nested drag-drop** | Games within days is hierarchy enough | Keep flat list per day section |
|
||||
| **Free-form canvas positioning** | Not applicable to linear itinerary | Stick to list reordering |
|
||||
| **Real-time collaborative drag** | Massive sync complexity | Single-user editing |
|
||||
| **Drag-to-resize** | Items don't have variable size | Fixed item heights |
|
||||
| **Custom drag preview images** | Native preview is sufficient | Use default lifted appearance |
|
||||
| **Physics-based spring animations** | Overkill for list reordering | Simple ease-out is fine |
|
||||
|
||||
---
|
||||
|
||||
## Interactions to Support
|
||||
|
||||
Specific drag scenarios for your itinerary context.
|
||||
|
||||
### Scenario 1: Move Custom Item Within Same Day
|
||||
|
||||
**User intent:** Reorder "Dinner at Lou Malnati's" from after to before the Cubs game
|
||||
|
||||
**Expected behavior:**
|
||||
1. Long-press on custom item (300ms) - haptic feedback
|
||||
2. Item lifts (shadow + scale), ghost remains at origin
|
||||
3. Drag within day section - insertion line appears between valid positions
|
||||
4. Games and travel segments shuffle with 100ms animation
|
||||
5. Drop - item snaps into place, haptic confirms
|
||||
|
||||
**Constraints:**
|
||||
- Custom item can move anywhere within the day
|
||||
- Cannot move before/after day header
|
||||
- Cannot replace or overlay a game (games are fixed)
|
||||
|
||||
### Scenario 2: Move Custom Item to Different Day
|
||||
|
||||
**User intent:** Move hotel check-in from Day 2 to Day 1
|
||||
|
||||
**Expected behavior:**
|
||||
1. Long-press and lift
|
||||
2. Drag toward Day 1 section
|
||||
3. Auto-scroll if Day 1 is off-screen
|
||||
4. Insertion line appears at valid positions in Day 1
|
||||
5. Day 2 collapses to show item removed; Day 1 expands
|
||||
6. Drop - item now in Day 1
|
||||
|
||||
**Constraints:**
|
||||
- Can cross day boundaries
|
||||
- Still cannot land on games
|
||||
|
||||
### Scenario 3: Move Travel Segment (Constrained)
|
||||
|
||||
**User intent:** Move "Drive: Chicago to Milwaukee" earlier in the day
|
||||
|
||||
**Expected behavior:**
|
||||
1. Long-press on travel segment
|
||||
2. Item lifts (possibly with different visual treatment since it's constrained)
|
||||
3. Insertion line only appears at **valid** positions (before/after games it connects)
|
||||
4. Invalid positions show no insertion line (or dimmed indicator)
|
||||
5. If dropped at invalid position, item animates back to origin
|
||||
|
||||
**Constraints:**
|
||||
- Travel segments connect stadiums/locations
|
||||
- Can only move within logical route order
|
||||
- Must validate position before showing insertion indicator
|
||||
|
||||
### Scenario 4: Attempt to Move Fixed Item (Game)
|
||||
|
||||
**User intent:** User tries to drag a game (not allowed)
|
||||
|
||||
**Expected behavior:**
|
||||
1. Long-press on game item
|
||||
2. **No lift animation** - item doesn't respond as draggable
|
||||
3. Optionally: subtle shake or tooltip "Games cannot be reordered"
|
||||
4. User understands this item is fixed
|
||||
|
||||
**Visual differentiation:**
|
||||
- Fixed items should NOT have drag handles
|
||||
- Could have different visual treatment (no grip icon, different background)
|
||||
|
||||
### Scenario 5: Drag to Invalid Zone
|
||||
|
||||
**User intent:** User drags custom item but releases over a game
|
||||
|
||||
**Expected behavior:**
|
||||
1. Item is being dragged
|
||||
2. Hovers over game - no insertion line appears (invalid)
|
||||
3. User releases
|
||||
4. Item animates back to origin (~200ms)
|
||||
5. Optional: brief error state or haptic warning
|
||||
|
||||
---
|
||||
|
||||
## Visual States Summary
|
||||
|
||||
| Element State | Visual Treatment |
|
||||
|--------------|------------------|
|
||||
| **Resting (draggable)** | Normal appearance, optional drag handle icon on hover/focus |
|
||||
| **Resting (fixed)** | Normal, but NO drag handle; visually distinct |
|
||||
| **Lifted/grabbed** | Elevated (shadow), slight scale up (1.02-1.05), optional tilt |
|
||||
| **Ghost at origin** | Semi-transparent (30-50% opacity) or outlined placeholder |
|
||||
| **Insertion line** | Accent-colored horizontal line, ~2px height, bleeds past edges |
|
||||
| **Invalid drop zone** | No insertion line; item over zone dims or shows warning |
|
||||
| **Drop zone ready** | Subtle background color shift when any drag starts |
|
||||
| **Dropped/success** | Snaps to place, haptic feedback, ghost disappears |
|
||||
| **Cancelled/error** | Returns to origin with animation, optional warning haptic |
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Requirements
|
||||
|
||||
| Requirement | Implementation | Priority |
|
||||
|-------------|----------------|----------|
|
||||
| **VoiceOver reordering** | accessibilityActions with "Move Up" / "Move Down" | High |
|
||||
| **Rotor integration** | Actions appear in VoiceOver rotor | High |
|
||||
| **Focus management** | Focus follows moved item after reorder | Medium |
|
||||
| **Live region announcements** | Announce position change ("Item moved to position 3") | Medium |
|
||||
| **Fallback buttons** | Optional up/down arrows as visual alternative | Low (nice to have) |
|
||||
|
||||
SwiftUI example for accessibility:
|
||||
```swift
|
||||
.accessibilityAction(named: "Move Up") { moveItemUp(item) }
|
||||
.accessibilityAction(named: "Move Down") { moveItemDown(item) }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mobile-Specific Considerations
|
||||
|
||||
| Concern | Solution |
|
||||
|---------|----------|
|
||||
| **Fat finger problem** | Minimum 44x44pt touch targets; drag handles at least 44pt wide |
|
||||
| **Scroll vs. drag conflict** | Long-press delay (300-500ms) distinguishes intent |
|
||||
| **Viewport limitations** | Auto-scroll at edges (40px threshold), speed increases near edge |
|
||||
| **One-handed use** | Consider "Move to..." button as alternative to long-distance drags |
|
||||
| **Accidental drops** | Generous drop zones; magnetic snap; undo option |
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
| Anti-Pattern | Why Bad | Do Instead |
|
||||
|--------------|---------|------------|
|
||||
| **Edge-to-edge shuffle trigger** | Feels "twitchy", items move unexpectedly | Use center-overlap-edge trigger |
|
||||
| **Instant reshuffle (no animation)** | Disorienting, hard to track what moved | 100ms animated transitions |
|
||||
| **No ghost/placeholder** | User loses context of original position | Always show origin indicator |
|
||||
| **Drag handle too small** | Frustrating on touch | Minimum 44pt, ideally larger |
|
||||
| **Remove item during drag** | Anxiety - "where did it go?" | Keep ghost visible at origin |
|
||||
| **Scroll too fast at edges** | Overshoots, loses control | Gradual speed increase |
|
||||
| **No invalid feedback** | User thinks interaction is broken | Clear visual/haptic for invalid drops |
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
**High Confidence (verified with multiple authoritative sources):**
|
||||
- [Smart Interface Design Patterns - Drag and Drop UX](https://smart-interface-design-patterns.com/articles/drag-and-drop-ux/)
|
||||
- [Atlassian Pragmatic Drag and Drop Design Guidelines](https://atlassian.design/components/pragmatic-drag-and-drop/design-guidelines/)
|
||||
- [Pencil & Paper - Drag & Drop UX Design Best Practices](https://www.pencilandpaper.io/articles/ux-pattern-drag-and-drop)
|
||||
- [Nielsen Norman Group - Drag and Drop: How to Design for Ease of Use](https://www.nngroup.com/articles/drag-drop/)
|
||||
|
||||
**Medium Confidence (single authoritative source):**
|
||||
- [LogRocket - Designing Drag and Drop UIs](https://blog.logrocket.com/ux-design/drag-and-drop-ui-examples/)
|
||||
- [Darin Senneff - Designing a Reorderable List Component](https://www.darins.page/articles/designing-a-reorderable-list-component)
|
||||
- [Apple Human Interface Guidelines - Drag and Drop](https://developer.apple.com/design/human-interface-guidelines/drag-and-drop)
|
||||
|
||||
**Low Confidence (community patterns):**
|
||||
- Various SwiftUI implementation guides (verify APIs against current documentation)
|
||||
- Trello UX patterns referenced in multiple articles (de facto standard)
|
||||
Reference in New Issue
Block a user