---
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"
---
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.
@~/.claude/get-shit-done/workflows/execute-plan.md
@~/.claude/get-shit-done/templates/summary.md
@.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
Task 1: Migrate to Modern Drag-Drop Delegates
SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
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.
Build succeeds: `xcodebuild -project SportsTime.xcodeproj -scheme SportsTime -destination 'platform=iOS Simulator,name=iPhone 17,OS=26.2' build`
ItineraryTableViewController conforms to UITableViewDragDelegate and UITableViewDropDelegate with all required methods implemented.
Task 2: Implement Lift Animation with Scale, Shadow, and Tilt
SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
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
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
Dragged items show lift animation with scale (1.025x), shadow, and tilt (2 degrees) on grab, and settle animation on drop.
Task 3: Add Haptic Feedback for Grab and Drop
SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
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.
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
Light haptic fires on grab, medium haptic fires on successful drop. Zone transition haptics continue working for valid/invalid zone crossing.
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)
- 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