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>
This commit is contained in:
220
.planning/phases/04-drag-interaction/04-01-PLAN.md
Normal file
220
.planning/phases/04-drag-interaction/04-01-PLAN.md
Normal file
@@ -0,0 +1,220 @@
|
||||
---
|
||||
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>
|
||||
Reference in New Issue
Block a user