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>
This commit is contained in:
Trey t
2026-01-18 13:16:07 -06:00
parent fc6943a6f9
commit 6f09c0abcb
5 changed files with 1515 additions and 0 deletions

291
.planning/research/STACK.md Normal file
View File

@@ -0,0 +1,291 @@
# 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<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:
```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 |