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

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
SportsTime/Features/Trip/Views/ItineraryTableViewController.swift
true
truths artifacts key_links
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
path provides contains
SportsTime/Features/Trip/Views/ItineraryTableViewController.swift UITableViewDragDelegate and UITableViewDropDelegate implementation UITableViewDragDelegate
from to via pattern
ItineraryTableViewController ItineraryConstraints.isValidPosition dropSessionDidUpdate validation 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.

<execution_context> @/.claude/get-shit-done/workflows/execute-plan.md @/.claude/get-shit-done/templates/summary.md </execution_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 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:

    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:

    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)

<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>
After completion, create `.planning/phases/04-drag-interaction/04-01-SUMMARY.md`