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>
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.
Recommended APIs
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:
- Conform to
UITableViewDropDelegate - Set
tableView.dropDelegate = self - Implement required delegate methods
- 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.onMoveonly 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
flatItemsarray 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.localObjectprovides 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:
- Coordinator should be
@MainActor- Delegate callbacks occur on main thread - Callbacks are closures - Already work correctly with Swift 6
- 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
Option A: Extend Existing UITableViewController (Recommended)
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
Summary: Recommended Stack
| 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
- UITableViewDragDelegate
- UITableViewDropDelegate
- Supporting drag and drop in table views
- Adopting drag and drop using SwiftUI
Technical Articles
- Using Drag and Drop on UITableView for reorder
- Drag to Reorder in UITableView with Diffable Datasource
- Coding for iOS 11: How to drag & drop into collections & tables
- SwiftUI in iOS 26 - What's new from WWDC 2025
- Drag and drop transferable data in SwiftUI
SwiftUI Limitations References
- Dragging list rows between sections - Apple Forums
- How to let users move rows in a list - Hacking with Swift
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 |