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>
8.9 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | |||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 04-drag-interaction | 01 | execute | 1 |
|
true |
|
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.
<execution_context>
@/.claude/get-shit-done/workflows/execute-plan.md
@/.claude/get-shit-done/templates/summary.md
</execution_context>
-
In
setupTableView():- Add
tableView.dragDelegate = self - Add
tableView.dropDelegate = self - Add
tableView.dragInteractionEnabled = true(required on iPhone) - Keep
isEditing = truefor visual consistency
- Add
-
Create
DragContextclass to store drag state: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 } } -
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 existingbeginDrag(at:).dragPreviewParametersForRowAt:- Return UIDragPreviewParameters with rounded corners (cornerRadius: 12).dragSessionWillBegin:- Trigger grab haptic via existing feedbackGenerator.
-
Add extension conforming to
UITableViewDropDelegate:performDropWith coordinator:- Extract source and destination from coordinator. Call existing move logic (flatItems remove/insert). CallendDrag(). Animate drop withcoordinator.drop(item.dragItem, toRowAt:).dropSessionDidUpdate session:withDestinationIndexPath:- Use existingcomputeValidDestinationRowsProposed()andnearestValue()logic to validate position. Return.movewith.insertAtDestinationIndexPathfor valid,.forbiddenfor invalid.dropSessionDidEnd:- CallendDrag()to clean up.
-
Keep existing
canMoveRowAtandmoveRowAtmethods 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.
-
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))
- Perspective:
- Add shadow: offset (0, 8), radius 16, opacity 0.25, masksToBounds = false
- Return configured snapshot
- Create snapshot using
-
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 = 0to hide original during drag
-
In
dragSessionWillBegin::- Retrieve DragContext from session.localContext
- Get cell for source indexPath
- Create snapshot, add to tableView.superview
- Animate lift
- Store snapshot in context
-
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:
- Drag a travel or custom item
- Item lifts with visible scale and shadow
- Slight 3D tilt is visible
- Drop settles smoothly Dragged items show lift animation with scale (1.025x), shadow, and tilt (2 degrees) on grab, and settle animation on drop.
-
The controller already has
feedbackGenerator = UIImpactFeedbackGenerator(style: .medium). Add a second generator:private let lightHaptic = UIImpactFeedbackGenerator(style: .light) private let mediumHaptic = UIImpactFeedbackGenerator(style: .medium) -
In
dragSessionWillBegin::- Call
lightHaptic.prepare()just before drag starts - Call
lightHaptic.impactOccurred()when drag begins
- Call
-
In
performDropWith:(successful drop):- Call
mediumHaptic.impactOccurred()after drop animation starts
- Call
-
Ensure prepare() is called proactively:
- In
itemsForBeginning:at:before returning items, calllightHaptic.prepare()andmediumHaptic.prepare()to reduce haptic latency
- In
-
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):
- Grab item - feel light tap
- Drop item - feel medium tap
- 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.
- Build succeeds without warnings
- Drag a travel item:
- Lifts with scale, shadow, tilt
- Light haptic on grab
- Can only drop in valid day range
- Medium haptic on drop
- Drag a custom item:
- Same lift behavior
- Can drop anywhere except on headers
- Try to drag a game or header:
- No drag interaction (returns empty array)
- Existing constraint validation still works (invalid positions clamped to nearest valid)
<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>