Files
SportstimeAPI/.planning/research/STACK.md
Trey t 6f09c0abcb docs: complete project research for itinerary editor
Files:
- STACK.md - UITableView drag-drop APIs, SwiftUI bridging patterns
- FEATURES.md - Table stakes UX (lift, insertion line, haptics), polish features
- ARCHITECTURE.md - 5-layer semantic positioning architecture
- PITFALLS.md - Critical pitfalls (row vs semantic, travel as structural)
- SUMMARY.md - Executive synthesis with roadmap implications

Key findings:
- Stack: UITableView + UIHostingConfiguration (existing pattern validated)
- Architecture: Semantic (day, sortOrder) model, not row indices
- Critical pitfall: Row indices are ephemeral; semantic positions are truth

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

12 KiB

Stack Research: UITableView Drag-Drop for Itinerary Editor

Project: SportsTime Itinerary Editor Researched: 2026-01-18 Overall Confidence: HIGH (existing implementation in codebase + stable APIs)

Executive Summary

The SportsTime codebase already contains a production-quality UITableView drag-drop implementation in ItineraryTableViewController.swift and ItineraryTableViewWrapper.swift. This research validates that approach and documents the recommended stack for extending it to support external drops.

Key Finding: The existing implementation uses the traditional canMoveRowAt/moveRowAt approach with tableView.isEditing = true. For external drops (from outside the table), the codebase will need to add UITableViewDropDelegate protocol conformance.


Core APIs (Already in Use)

API Purpose Confidence
UITableViewController Native table with built-in drag handling HIGH
tableView.isEditing = true Enables drag handles on rows HIGH
canMoveRowAt: Controls which rows show drag handles HIGH
moveRowAt:to: Called when reorder completes HIGH
targetIndexPathForMoveFromRowAt:toProposedIndexPath: Real-time validation during drag HIGH
UIHostingConfiguration Embeds SwiftUI views in cells HIGH

Rationale: These APIs provide the smooth, native iOS reordering experience with real-time insertion line feedback. The existing implementation demonstrates this working well.

APIs Needed for External Drops

API Purpose When to Use Confidence
UITableViewDropDelegate Accept drops from outside the table Required for external drops HIGH
UITableViewDragDelegate Provide drag items (not strictly needed if only receiving) Optional HIGH
dropSessionDidUpdate(_:withDestinationIndexPath:) Validate drop during hover Shows insertion feedback for external drags HIGH
performDropWith(_:) Handle external drop completion Called only for external drops (not internal moves) HIGH
canHandle(_:) Validate drop session types Filter what can be dropped HIGH
NSItemProvider Data transfer wrapper Encodes dragged item data HIGH
UIDragItem.localObject In-app optimization Avoids encoding when drag is same-app HIGH

Rationale: For external drops, UITableViewDropDelegate is required. The key insight from research: when both moveRowAt: and performDropWith: are implemented, UIKit automatically routes internal reorders through moveRowAt: and external drops through performDropWith:. This is documented behavior.


SwiftUI Integration Pattern

Current Pattern (Validated)

The codebase uses UIViewControllerRepresentable with a Coordinator pattern:

struct ItineraryTableViewWrapper<HeaderContent: View>: UIViewControllerRepresentable {
    // Callbacks for data mutations (lifted state)
    var onTravelMoved: ((String, Int, Double) -> Void)?
    var onCustomItemMoved: ((UUID, Int, Double) -> Void)?

    class Coordinator {
        var headerHostingController: UIHostingController<HeaderContent>?
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    func makeUIViewController(context: Context) -> ItineraryTableViewController {
        let controller = ItineraryTableViewController(style: .plain)
        // Configure callbacks
        return controller
    }

    func updateUIViewController(_ controller: ItineraryTableViewController, context: Context) {
        // Push new data to controller
        controller.reloadData(days: days, ...)
    }
}

Confidence: HIGH (implemented and working)

For External Drops: Callback Extension

Add a new callback for external drops:

var onExternalItemDropped: ((ExternalDropItem, Int, Double) -> Void)?
// Parameters: dropped item, target day, target sortOrder

The ItineraryTableViewController would need to:

  1. Conform to UITableViewDropDelegate
  2. Set tableView.dropDelegate = self
  3. Implement required delegate methods
  4. Call the callback when external drop completes

Confidence: HIGH (standard pattern extension)


What to Avoid

Anti-Pattern 1: SwiftUI-Only Drag-Drop for Complex Reordering

What: Using .draggable() / .dropDestination() / .onMove() directly in SwiftUI List

Why Avoid:

  • No real-time insertion line feedback during drag (item only moves on drop)
  • ForEach.onMove only works within a single section
  • Limited control over valid drop positions during drag
  • iPhone has additional limitations for SwiftUI List drag-drop

Evidence: The codebase documentation explicitly states: "SwiftUI's drag-and-drop APIs have significant limitations for complex reordering"

Confidence: HIGH

Anti-Pattern 2: Third-Party Reordering Libraries

What: Using libraries like SwiftReorder, LPRTableView, TableViewDragger

Why Avoid:

  • Compatibility issues with recent iOS versions reported
  • Built-in UITableView drag-drop (iOS 11+) is more reliable
  • Additional dependency for functionality that's native

Evidence: Multiple search results recommend "use the built-in UITableView drag and drop API" over third-party libraries

Confidence: MEDIUM (anecdotal reports)

Anti-Pattern 3: Mixing Diffable Data Source with Manual Array Updates

What: Using UITableViewDiffableDataSource but manually manipulating the array in moveRowAt:

Why Avoid:

  • Risk of data source inconsistency
  • Diffable data sources have their own update patterns
  • The current implementation uses manual flatItems array management which works correctly

If Using Diffable Data Source: Must reconcile changes through snapshot mechanism, not direct array manipulation

Confidence: MEDIUM

Anti-Pattern 4: Ignoring localObject for Same-App Drops

What: Always encoding/decoding NSItemProvider data even for internal drops

Why Avoid:

  • Unnecessary overhead for same-app transfers
  • UIDragItem.localObject provides direct object access without serialization
  • More complex code for no benefit

Best Practice: Check localObject first, fall back to NSItemProvider decoding only for cross-app drops

Confidence: HIGH


iOS 26 Considerations

New SwiftUI Drag-Drop Modifiers (iOS 26)

iOS 26 introduces improved SwiftUI drag-drop modifiers:

  • .draggable(containerItemID:) - Marks items as draggable
  • .dragContainer(for:selection:) - Defines container and selection
  • .dragConfiguration() - Controls behavior (allowMove, allowDelete)
  • .onDragSessionUpdated() - Handles drag phases
  • .dragPreviewsFormation(.stack) - Customizes preview

Assessment: These are promising for simpler use cases, particularly macOS file management UIs. However, for the existing UITableView-based itinerary editor:

Recommendation: Keep the UITableView approach. The new SwiftUI modifiers don't provide the same level of control needed for:

  • Constraint-aware drop validation (travel can only go on certain days)
  • Real-time insertion line between specific rows
  • Semantic positioning (day + sortOrder) vs row indices

Confidence: MEDIUM (iOS 26 APIs are new, full capabilities not fully documented)

Swift 6 Concurrency Considerations

The existing ItineraryTableViewController is a final class (not actor). Key considerations:

  1. Coordinator should be @MainActor - Delegate callbacks occur on main thread
  2. Callbacks are closures - Already work correctly with Swift 6
  3. No async operations during drag - Validation is synchronous, which is correct

No changes required for Swift 6 compliance in the existing implementation.

Confidence: HIGH


Architecture Decision: Two Approaches for External Drops

Add UITableViewDropDelegate to ItineraryTableViewController:

extension ItineraryTableViewController: UITableViewDropDelegate {
    func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
        // Accept your custom item types
        return session.canLoadObjects(ofClass: ItineraryItemTransferable.self)
    }

    func tableView(_ tableView: UITableView,
                   dropSessionDidUpdate session: UIDropSession,
                   withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
        // Return .insertAtDestinationIndexPath for insertion line feedback
        return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
    }

    func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) {
        // Extract item and calculate semantic position
        // Call onExternalItemDropped callback
    }
}

Pros:

  • Builds on existing, working implementation
  • Minimal code changes
  • Maintains semantic positioning logic

Cons:

  • None significant

Option B: SwiftUI Overlay for Drag Source

If the external drag SOURCE is a SwiftUI view (e.g., a "suggestions" panel):

// In SwiftUI
SuggestionCard(item: item)
    .draggable(item) {
        SuggestionPreview(item: item)
    }

The UITableView receives this via UITableViewDropDelegate as above.

Note: This hybrid approach works well - SwiftUI provides the drag source, UIKit receives the drop.

Confidence: HIGH


Component Recommendation Rationale
Table View UITableViewController Native drag handles, real-time feedback
Internal Reorder canMoveRowAt / moveRowAt Already working, proven
External Drops Add UITableViewDropDelegate Required for external drops
SwiftUI Bridge UIViewControllerRepresentable Already working
Cell Content UIHostingConfiguration SwiftUI views in UIKit cells
State Management Lifted callbacks to parent Unidirectional data flow
Drag Source (external) SwiftUI .draggable() Simple for source views
Position Model (day, sortOrder) semantics Already working, robust

Sources

Official Documentation

Technical Articles

SwiftUI Limitations References


Confidence Assessment

Area Confidence Reason
Core UITableView Drag APIs HIGH Stable since iOS 11, extensive documentation
External Drop via UITableViewDropDelegate HIGH Standard documented pattern
SwiftUI Bridge Pattern HIGH Already implemented and working in codebase
iOS 26 SwiftUI Improvements MEDIUM New APIs, limited production experience
Swift 6 Compatibility HIGH Existing code is already compliant
Third-party library avoidance MEDIUM Based on community reports, not direct testing