Files
SportstimeAPI/.planning/phases/04-drag-interaction/04-01-PLAN.md
Trey t 23788a44d2 docs(04): create phase 4 drag interaction plans
Phase 04: Drag Interaction
- 2 plans in 2 waves
- Plan 01: Migrate to modern drag-drop delegates, lift animation, haptics
- Plan 02: Themed insertion line, invalid zone feedback, snap-back animation

Ready for execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 16:39:06 -06:00

221 lines
8.9 KiB
Markdown

---
phase: 04-drag-interaction
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
autonomous: true
must_haves:
truths:
- "User sees item lift with subtle scale and shadow when grabbed"
- "User feels light haptic on grab"
- "User feels medium haptic on successful drop"
- "Items can only be dropped in constraint-valid positions"
artifacts:
- path: "SportsTime/Features/Trip/Views/ItineraryTableViewController.swift"
provides: "UITableViewDragDelegate and UITableViewDropDelegate implementation"
contains: "UITableViewDragDelegate"
key_links:
- from: "ItineraryTableViewController"
to: "ItineraryConstraints.isValidPosition"
via: "dropSessionDidUpdate validation"
pattern: "constraints\\.isValidPosition"
---
<objective>
Migrate from legacy reordering methods to modern UITableViewDragDelegate/UITableViewDropDelegate and implement core drag interactions: lift animation with scale/shadow/tilt, grab/drop haptics, and constraint-aware drop proposals.
Purpose: Modern drag-drop delegates unlock custom drag previews and precise drop validation. This establishes the foundation for Phase 4's rich visual feedback.
Output: ItineraryTableViewController with working modern drag-drop that feels responsive with lift animation and haptic feedback.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/04-drag-interaction/04-CONTEXT.md
@.planning/phases/04-drag-interaction/04-RESEARCH.md
@SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
@SportsTime/Core/Models/Domain/ItineraryConstraints.swift
@SportsTime/Core/Theme/Theme.swift
</context>
<tasks>
<task type="auto">
<name>Task 1: Migrate to Modern Drag-Drop Delegates</name>
<files>SportsTime/Features/Trip/Views/ItineraryTableViewController.swift</files>
<action>
Replace the legacy reordering approach with modern UITableViewDragDelegate and UITableViewDropDelegate:
1. In `setupTableView()`:
- Add `tableView.dragDelegate = self`
- Add `tableView.dropDelegate = self`
- Add `tableView.dragInteractionEnabled = true` (required on iPhone)
- Keep `isEditing = true` for visual consistency
2. Create `DragContext` class to store drag state:
```swift
private class DragContext {
let item: ItineraryRowItem
let sourceIndexPath: IndexPath
let originalFrame: CGRect
var snapshot: UIView?
init(item: ItineraryRowItem, sourceIndexPath: IndexPath, originalFrame: CGRect) {
self.item = item
self.sourceIndexPath = sourceIndexPath
self.originalFrame = originalFrame
}
}
```
3. Add extension conforming to `UITableViewDragDelegate`:
- `itemsForBeginning session:at:` - Return empty array for non-reorderable items (games, headers). For reorderable items, create UIDragItem with NSItemProvider, store DragContext in session.localContext, call existing `beginDrag(at:)`.
- `dragPreviewParametersForRowAt:` - Return UIDragPreviewParameters with rounded corners (cornerRadius: 12).
- `dragSessionWillBegin:` - Trigger grab haptic via existing feedbackGenerator.
4. Add extension conforming to `UITableViewDropDelegate`:
- `performDropWith coordinator:` - Extract source and destination from coordinator. Call existing move logic (flatItems remove/insert). Call `endDrag()`. Animate drop with `coordinator.drop(item.dragItem, toRowAt:)`.
- `dropSessionDidUpdate session:withDestinationIndexPath:` - Use existing `computeValidDestinationRowsProposed()` and `nearestValue()` logic to validate position. Return `.move` with `.insertAtDestinationIndexPath` for valid, `.forbidden` for invalid.
- `dropSessionDidEnd:` - Call `endDrag()` to clean up.
5. Keep existing `canMoveRowAt` and `moveRowAt` methods as fallback but the new delegates should take precedence.
Key: Reuse existing constraint validation logic (computeValidDestinationRowsProposed, nearestValue, checkZoneTransition). The delegates wire into that existing infrastructure.
</action>
<verify>
Build succeeds: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
</verify>
<done>
ItineraryTableViewController conforms to UITableViewDragDelegate and UITableViewDropDelegate with all required methods implemented.
</done>
</task>
<task type="auto">
<name>Task 2: Implement Lift Animation with Scale, Shadow, and Tilt</name>
<files>SportsTime/Features/Trip/Views/ItineraryTableViewController.swift</files>
<action>
Add custom lift animation when drag begins per CONTEXT.md decisions (iOS Reminders style, quick/snappy, 1.02-1.03x scale, 2-3 degree tilt):
1. Add private helper method `createLiftedSnapshot(for cell:) -> UIView`:
- Create snapshot using `cell.snapshotView(afterScreenUpdates: false)`
- Set frame to cell.frame
- Apply CATransform3D with:
- Perspective: `transform.m34 = -1.0 / 500.0`
- Scale: 1.025 (middle of 1.02-1.03 range)
- Tilt: 2.0 degrees around Y axis (`CATransform3DRotate(transform, 2.0 * .pi / 180.0, 0, 1, 0)`)
- Add shadow: offset (0, 8), radius 16, opacity 0.25, masksToBounds = false
- Return configured snapshot
2. Add private helper method `animateLift(for cell:, snapshot:)`:
- Start snapshot with identity transform and shadowOpacity 0
- Use UIView.animate with duration 0.15, spring damping 0.85, velocity 0.5
- Animate to lifted transform and shadowOpacity 0.25
- Set `cell.alpha = 0` to hide original during drag
3. In `dragSessionWillBegin:`:
- Retrieve DragContext from session.localContext
- Get cell for source indexPath
- Create snapshot, add to tableView.superview
- Animate lift
- Store snapshot in context
4. Add `animateDrop(snapshot:, to destination:)` for drop animation:
- Animate snapshot back to identity transform
- Fade out shadowOpacity
- Remove snapshot on completion
- Restore cell.alpha = 1
Key timing values per CONTEXT.md:
- Lift: 0.15s spring
- Drop: 0.2s spring with damping 0.8
</action>
<verify>
Run on simulator and verify:
1. Drag a travel or custom item
2. Item lifts with visible scale and shadow
3. Slight 3D tilt is visible
4. Drop settles smoothly
</verify>
<done>
Dragged items show lift animation with scale (1.025x), shadow, and tilt (2 degrees) on grab, and settle animation on drop.
</done>
</task>
<task type="auto">
<name>Task 3: Add Haptic Feedback for Grab and Drop</name>
<files>SportsTime/Features/Trip/Views/ItineraryTableViewController.swift</files>
<action>
Enhance haptic feedback to match CONTEXT.md decisions (light on grab, medium on drop):
1. The controller already has `feedbackGenerator = UIImpactFeedbackGenerator(style: .medium)`. Add a second generator:
```swift
private let lightHaptic = UIImpactFeedbackGenerator(style: .light)
private let mediumHaptic = UIImpactFeedbackGenerator(style: .medium)
```
2. In `dragSessionWillBegin:`:
- Call `lightHaptic.prepare()` just before drag starts
- Call `lightHaptic.impactOccurred()` when drag begins
3. In `performDropWith:` (successful drop):
- Call `mediumHaptic.impactOccurred()` after drop animation starts
4. Ensure prepare() is called proactively:
- In `itemsForBeginning:at:` before returning items, call `lightHaptic.prepare()` and `mediumHaptic.prepare()` to reduce haptic latency
5. Keep existing zone transition haptics in `checkZoneTransition()` for entering/leaving valid zones.
Note: The existing feedbackGenerator can be replaced or kept for backward compatibility with existing code paths.
</action>
<verify>
Test on physical device (simulator has no haptics):
1. Grab item - feel light tap
2. Drop item - feel medium tap
3. Drag over invalid zone - feel warning haptic
</verify>
<done>
Light haptic fires on grab, medium haptic fires on successful drop. Zone transition haptics continue working for valid/invalid zone crossing.
</done>
</task>
</tasks>
<verification>
After completing all tasks:
1. Build succeeds without warnings
2. Drag a travel item:
- Lifts with scale, shadow, tilt
- Light haptic on grab
- Can only drop in valid day range
- Medium haptic on drop
3. Drag a custom item:
- Same lift behavior
- Can drop anywhere except on headers
4. Try to drag a game or header:
- No drag interaction (returns empty array)
5. Existing constraint validation still works (invalid positions clamped to nearest valid)
</verification>
<success_criteria>
- DRAG-01: Lift animation on grab (shadow + slight scale) - IMPLEMENTED
- DRAG-06: Haptic feedback on grab (light) and drop (medium) - IMPLEMENTED
- DRAG-08: Slight tilt during drag (2-3 degrees) - IMPLEMENTED
- Foundation for DRAG-02 through DRAG-05 established via delegate pattern
</success_criteria>
<output>
After completion, create `.planning/phases/04-drag-interaction/04-01-SUMMARY.md`
</output>