# 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: ```swift struct ItineraryTableViewWrapper: UIViewControllerRepresentable { // Callbacks for data mutations (lifted state) var onTravelMoved: ((String, Int, Double) -> Void)? var onCustomItemMoved: ((UUID, Int, Double) -> Void)? class Coordinator { var headerHostingController: UIHostingController? } 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: ```swift 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 ### Option A: Extend Existing UITableViewController (Recommended) Add `UITableViewDropDelegate` to `ItineraryTableViewController`: ```swift 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): ```swift // 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](https://developer.apple.com/documentation/uikit/uitableviewdragdelegate) - [UITableViewDropDelegate](https://developer.apple.com/documentation/uikit/uitableviewdropdelegate) - [Supporting drag and drop in table views](https://developer.apple.com/documentation/uikit/views_and_controls/table_views/supporting_drag_and_drop_in_table_views) - [Adopting drag and drop using SwiftUI](https://developer.apple.com/documentation/SwiftUI/Adopting-drag-and-drop-using-SwiftUI) ### Technical Articles - [Using Drag and Drop on UITableView for reorder](https://rderik.com/blog/using-drag-and-drop-on-uitableview-for-reorder/) - [Drag to Reorder in UITableView with Diffable Datasource](https://swiftjectivec.com/Tableview-Diffable-Datasource-Drag-to-Reorder/) - [Coding for iOS 11: How to drag & drop into collections & tables](https://hackernoon.com/drag-it-drop-it-in-collection-table-ios-11-6bd28795b313) - [SwiftUI in iOS 26 - What's new from WWDC 2025](https://differ.blog/p/swift-ui-in-ios-26-what-s-new-from-wwdc-2025-819b42) - [Drag and drop transferable data in SwiftUI](https://swiftwithmajid.com/2023/04/05/drag-and-drop-transferable-data-in-swiftui/) ### SwiftUI Limitations References - [Dragging list rows between sections - Apple Forums](https://developer.apple.com/forums/thread/674393) - [How to let users move rows in a list - Hacking with Swift](https://www.hackingwithswift.com/quick-start/swiftui/how-to-let-users-move-rows-in-a-list) --- ## 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 |